/* Digital School Portal - Sidebar layer
   ============================================================================
   Loaded AFTER portal.css and BEFORE mobile.css (see templates/base.html).

   WHAT LIVES WHERE
     portal.css  owns the shell geometry: the fixed <nav> column, and the
                 offset it applies to .portal-main-column (margin-left) and
                 .portal-topbar (left). Those offsets now read --sb-wide /
                 --sb-rail from this file, so the page still lines up.
     this file   owns everything painted inside the column: brand row, sections,
                 links, groups, footer, and the collapsed rail.
     mobile.css  owns the <=767px off-canvas drawer.

   WHY THE RAIL CAN HOVER EXPAND WITHOUT MOVING THE PAGE
     The <nav> stays position:fixed, so widening it never reflows the content
     column (that column is offset by margin-left, which only changes when the
     rail is pinned open or closed). Collapsed puts the nav at --sb-rail; hover
     or keyboard focus drops the `collapsed` class, the nav grows back to
     --sb-wide as an overlay above the page, and `peeking` adds the shadow.
     Alpine in partials/_sidebar.html drives both classes from
     `sidebarCollapsed && !peek`.

   Every rule uses the existing theme variables, so dark mode and a school's
   brand colours carry over without a second palette. In the dark theme
   --color-border is deliberately transparent (depth comes from surface colours),
   which is why dividers use --color-border-inner and the tree guide uses
   --sb-line.
   ========================================================================== */

:root {
  --sb-rail: 64px;      /* collapsed: the width the page is offset by */
  --sb-wide: 250px;     /* pinned open / hover overlay — matches portal.css */
  --sb-speed: 180ms;
  --sb-line: var(--color-border-strong); /* visible tree guide in both themes */
}

/* ── Painted column ───────────────────────────────────────────────────────── */
.sb-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  min-height: 0;
  background: var(--color-surface);
  border-right: 1px solid var(--color-border-strong);
  overflow: hidden;
  transition: box-shadow var(--sb-speed) ease;
}

/* ── Brand row ────────────────────────────────────────────────────────────── */
.sb-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 56px;
  padding: 0 12px 0 16px;
  flex: 0 0 auto;
  border-bottom: 1px solid var(--color-border-inner, var(--color-border));
}

.sb-logo {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  object-fit: cover;
  flex: 0 0 auto;
}

.sb-logo-contain { object-fit: contain; }

.sb-brand-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sb-icon-btn {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  flex: 0 0 auto;
}

.sb-icon-btn:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}

.sb-icon-btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

.sb-icon-btn svg { width: 16px; height: 16px; }

/* Pin toggle only makes sense on a desktop rail; phones use the drawer close. */
.sb-collapse-btn { display: none; }
.sb-collapse-btn svg { transform: rotate(90deg); transition: transform var(--sb-speed); }
.sidebar-collapsed .sb-collapse-btn svg { transform: rotate(-90deg); }
.sb-mobile-close { display: inline-flex; }

/* ── Scroll area, sections, lists ─────────────────────────────────────────── */
.sb-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 4px 0 16px;
  scrollbar-width: thin;
}

.sb-section { padding: 0 8px; margin-top: 16px; }
.sb-section:first-child { margin-top: 8px; }

.sb-section-label {
  margin: 0 0 4px;
  padding: 0 14px;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  opacity: 0.8;
}

.sb-list,
.sb-children { list-style: none; margin: 0; padding: 0; }
.sb-list { display: flex; flex-direction: column; gap: 2px; }

/* ── Links ────────────────────────────────────────────────────────────────── */
.sb-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 40px;
  padding: 0 14px;
  border-radius: 10px;
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
  color: var(--color-text-muted);
  transition: background var(--sb-speed) ease, color var(--sb-speed) ease;
}

.sb-link:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}

.sb-link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

.sb-link.active {
  background: var(--color-surface-2);
  background: color-mix(in srgb, var(--color-primary) 12%, transparent);
  color: var(--color-primary-text);
  font-weight: 600;
}

.sb-link.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--color-primary);
}

.sb-icon {
  flex: 0 0 20px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.sb-icon svg { width: 20px; height: 20px; }

.sb-label {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A locked feature says which plan owns it, without a second line of prose. */
.sb-lock { display: inline-flex; opacity: 0.6; flex: 0 0 auto; }
.sb-lock svg { width: 14px; height: 14px; }

/* ── Groups ───────────────────────────────────────────────────────────────── */
.sb-group-header {
  display: flex;
  align-items: center;
  gap: 2px;
}

.sb-group-header .sb-link { flex: 1 1 auto; min-width: 0; }

.sb-chevron {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
}

.sb-chevron:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}

.sb-chevron:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

.sb-chevron svg { width: 16px; height: 16px; transition: transform var(--sb-speed); }
.sb-chevron[aria-expanded="true"] svg { transform: rotate(180deg); }

/* When a child is the page you are on, the header keeps only a quiet accent:
   the filled pill would highlight the group twice. */
.sb-group:has(.sb-child-link.active) > .sb-group-header .sb-link.active {
  background: transparent;
  color: var(--color-text);
  font-weight: 500;
}

.sb-group:has(.sb-child-link.active) > .sb-group-header .sb-link.active::before {
  display: none;
}

.sb-group:has(.sb-child-link.active) > .sb-group-header .sb-icon {
  color: var(--color-primary-text);
}

/* Children hang off a vertical guide line so the hierarchy is readable. */
.sb-children {
  position: relative;
  margin: 2px 0 4px 24px;
}

.sb-children::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: var(--sb-line);
  opacity: 0.5;
}

.sb-child-link {
  min-height: 34px;
  padding: 0 10px 0 22px;
  font-size: 0.8125rem;
}

.sb-child-link::after {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 10px;
  height: 1px;
  background: var(--sb-line);
}

.sb-child-link.active::before { display: none; }
.sb-child-link.active::after { background: var(--color-primary-text); width: 12px; }

/* ── Footer ───────────────────────────────────────────────────────────────── */
.sb-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid var(--color-border-inner, var(--color-border));
  flex: 0 0 auto;
}

.sb-user { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto; }
.sb-user-text { min-width: 0; }

.sb-user-name {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sb-user-role {
  margin: 0;
  font-size: 0.625rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sb-footer-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }

/* ── Desktop: rail, hover overlay, pinned open ─────────────────────────────
   >=768px is the same breakpoint mobile.css uses for the drawer, so the two
   layers never fight over the sidebar. */
@media (min-width: 768px) {
  .portal-sidebar.sb {
    z-index: 41; /* above the page, below the peeking overlay and the topbar */
    overflow: visible; /* the overlay is allowed to leave the wrapper */
    padding: 0;
  }

  .sb-collapse-btn { display: inline-flex; }
  .sb-mobile-close { display: none; }

  /* Hovering / tabbing into a pinned-closed rail: the nav is fixed, so growing
     it back to --sb-wide paints over the page instead of pushing it. */
  .portal-sidebar.sb.peeking { z-index: 60; }

  .sb.peeking .sb-panel {
    border-radius: 0 14px 14px 0;
    box-shadow: 12px 0 32px rgba(0, 0, 0, 0.18);
  }

  .sb.peeking .sb-label,
  .sb.peeking .sb-section-label { animation: sb-fade 160ms ease; }

  /* Collapsed rail: icons only, everything else stays in the DOM and focusable
     through the hover overlay. */
  .sb.collapsed .sb-label,
  .sb.collapsed .sb-section-label,
  .sb.collapsed .sb-chevron,
  .sb.collapsed .sb-children,
  .sb.collapsed .sb-lock,
  .sb.collapsed .sb-brand-name,
  .sb.collapsed .sb-collapse-btn,
  .sb.collapsed .sb-user-text { display: none !important; }

  .sb.collapsed .sb-link { justify-content: center; padding: 0; gap: 0; }
  .sb.collapsed .sb-link.active::before { left: -8px; }
  .sb.collapsed .sb-section {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--color-border-inner, var(--color-border));
  }
  .sb.collapsed .sb-section:first-child { border-top: 0; padding-top: 0; }
  .sb.collapsed .sb-brand { justify-content: center; padding: 0; }
  .sb.collapsed .sb-footer { flex-direction: column; padding: 10px 0; }
  .sb.collapsed .sb-footer-actions { flex-direction: column; }
}

/* ==========================================================================
   TOPBAR  (partials/_topbar.html)
   portal.css owns position, height, the left offset that clears the fixed
   sidebar, and z-index; this section owns appearance. The bar is a
   translucent, blurred surface closed by a hairline (no drop shadow), and
   every action is a 36px icon-only ghost button drawn with the sidebar's own
   sprite glyphs.
   ========================================================================== */
.portal-topbar.tb {
  background-color: var(--color-surface);
  background-color: color-mix(in srgb, var(--color-surface) 84%, transparent);
  border-bottom: 1px solid var(--color-border-inner);
  box-shadow: none;
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
}

.tb-title { flex: 1; min-width: 0; }

.tb-school {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tb-actions { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }

.tb-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--color-text-faint);
  text-decoration: none;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}

.tb-btn:hover { background: var(--color-surface-2); color: var(--color-text); }
.tb-btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.tb-btn svg { width: 19px; height: 19px; }

.tb-sep { width: 1px; height: 20px; margin: 0 8px; background: var(--color-border-input); }

.tb-hide-sm { display: none !important; }
@media (min-width: 640px) { .tb-hide-sm { display: inline-flex !important; } }

.tb-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  font-size: 9.5px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0;
  /* ring the badge out of the bar so it reads as a count, not a smudge */
  box-shadow: 0 0 0 2px var(--color-surface);
}

/* ── Cloud sync chip: a state dot + label, then the manual trigger ───────── */
.tb-sync {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 30px;
  margin-right: 2px;
  padding: 0 2px 0 11px;
  border-radius: 999px;
  background: var(--color-surface-2);
  background: color-mix(in srgb, var(--color-surface-2) 72%, transparent);
}

.sync-state {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--color-text-muted);
}

/* The same markup appears in the phone sheet at text-sm; only the chip is
   this small. */
.tb-sync .sync-state { font-size: 0.6875rem; }

.sync-dot {
  width: 7px;
  height: 7px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--dot, var(--color-text-faint));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--dot, transparent) 26%, transparent);
}

.sync-state--off { --dot: var(--color-text-faint); }
.sync-state--ok { --dot: var(--color-success); }
.sync-state--pending { --dot: var(--color-warning); }
.sync-state--busy { --dot: var(--color-primary-text); }
.sync-state--busy .sync-dot { animation: tb-pulse 1.4s ease-in-out infinite; }

@keyframes tb-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

.tb-sync-btn {
  width: 26px;
  height: 26px;
  border-radius: 50%;
}

.tb-sync-btn:hover {
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
}

.tb-sync-glyph { display: inline-flex; }
.tb-sync-btn svg { width: 15px; height: 15px; }

/* htmx puts .htmx-request on the button itself (hx-disabled-elt="this"), so the
   glyph gives way to a ring sized for the chip. portal.css's .sync-spinner is
   white-on-transparent and would vanish on a light chip, so it stays hidden. */
.tb-sync-btn .htmx-indicator {
  width: 15px;
  height: 15px;
  border-width: 2px;
  border-color: var(--color-text-faint);
  border-right-color: transparent;
}

.tb-sync-btn .sync-spinner { display: none; }
.tb-sync-btn.htmx-request .tb-sync-glyph { display: none; }
.tb-sync-btn:not(.htmx-request) .htmx-indicator { display: none; }

/* Dark is the default theme — it is the ABSENCE of data-theme on <html>, not
   data-theme="dark" — so the sun (i.e. "switch to light") shows by default and
   the moon only under html[data-theme="light"]. portal.js must never write
   into #theme-icon: the old emoji assignment wiped these two spans. */
.tb-sun { display: inline-flex; }
.tb-moon { display: none; }
html[data-theme="light"] .tb-sun { display: none; }
html[data-theme="light"] .tb-moon { display: inline-flex; }

/* The identity block is not interactive, so it carries no border or hover. */
.tb-user {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 4px 0 0;
  border-radius: 999px;
}

.tb-user-text { display: none; flex-direction: column; line-height: 1.15; min-width: 0; }
@media (min-width: 640px) { .tb-user-text { display: flex; } }
.tb-user-name { font-size: .8125rem; font-weight: 600; color: var(--color-text); white-space: nowrap; }
.tb-user-role { font-size: .6875rem; color: var(--color-text-faint); white-space: nowrap; }
@media (max-width: 639px) { .tb-user { padding: 0; } }

/* ==========================================================================
   NOTIFICATIONS  (communications/notifications.html + its partials)
   Cards keep portal-card (the mark-read hx-target is "closest .portal-card"),
   so their padding is tightened here rather than on the shared class.
   ========================================================================== */
.nt-page-head { margin-bottom: 20px; }
.nt-page-title { margin: 0; font-size: 1.75rem; font-weight: 700; color: var(--color-text); }
.nt-page-sub { margin: 4px 0 0; font-size: .875rem; color: var(--color-text-muted); }

.nt-section { margin-bottom: 24px; }

.nt-section-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.nt-stack { display: flex; flex-direction: column; gap: 10px; }

.nt-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--color-surface-2);
  color: var(--color-text-muted);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: 0;
}

.nt-toolbar {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  margin-bottom: 14px;
  border-radius: 12px;
  background: var(--color-surface-2);
}

.nt-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 12px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--color-text-muted);
  font-size: .8125rem;
  font-weight: 500;
  cursor: pointer;
}

.nt-tab.is-on { background: var(--color-surface); color: var(--color-text); box-shadow: var(--shadow-1); }
.nt-tab:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.nt-tab .nt-count { background: var(--color-surface-3); }

/* Cards. portal-card supplies bg, radius, shadow and a 1.5em padding we tighten
   here. A transparent 4px left border keeps read and unread text aligned, and
   matches the 4px stripe .feed-status-card already paints. */
.nt-card {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 14px 16px;
  border-left: 4px solid transparent;
}

.nt-card.is-unread {
  border-left-color: var(--color-primary-text);
  background: var(--color-surface);
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-surface));
}

.nt-card-action { align-items: center; }
.nt-body { flex: 1; min-width: 0; }
.nt-title-row { display: flex; align-items: center; gap: 8px; }
.nt-title { margin: 0; font-size: .9375rem; font-weight: 600; color: var(--color-text); }
.nt-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-primary-text); flex: 0 0 auto; }
.nt-msg { margin: 4px 0 0; font-size: .8125rem; line-height: 1.5; color: var(--color-text-muted); overflow-wrap: anywhere; }
.nt-msg-bad { color: var(--color-danger); }
.nt-meta { display: flex; align-items: center; gap: 12px; margin-top: 8px; font-size: .75rem; color: var(--color-text-faint); }
.nt-link { color: var(--color-primary-text); font-weight: 500; }
.nt-link:hover { color: var(--color-primary-text); text-decoration: underline; }

.nt-ico {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  background: var(--color-surface-2);
  color: var(--color-text-muted);
}

.feed-status-card .nt-ico { background: var(--color-surface-3); } /* card bg is surface-2 */
.nt-ico svg { width: 18px; height: 18px; }
.nt-card.is-unread .nt-ico { background: var(--color-surface-tint); color: var(--color-primary-text); }
.nt-ico-ok { color: var(--color-success); }
.nt-ico-bad { color: var(--color-danger); }
.nt-ico-warn { color: var(--color-warning); }

.nt-ico-solid {
  background: var(--color-primary);
  color: var(--color-on-primary);
  border-radius: 50%;
  width: 40px;
  height: 40px;
  flex-basis: 40px;
}

.nt-progress { height: 6px; margin-top: 8px; border-radius: 999px; background: var(--color-surface-3); overflow: hidden; }
.nt-progress-bar { height: 100%; background: var(--color-primary-text); transition: width 500ms; }

.nt-mark, .nt-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
}

.nt-mark:hover, .nt-x:hover { background: var(--color-surface-3); color: var(--color-text); }
.nt-mark:hover { color: var(--color-success); }
.nt-mark:focus-visible, .nt-x:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.nt-mark svg, .nt-x svg { width: 16px; height: 16px; }

/* feed-remove-btn (portal.css) is a bordered circle; keep the old look off the
   new X without dropping the class anything else may target. */
.nt-x.feed-remove-btn { border: 0; border-radius: 8px; width: 32px; height: 32px; }

.nt-empty { display: flex; flex-direction: column; align-items: center; padding: 40px 16px; text-align: center; }

.nt-empty-ico {
  width: 48px;
  height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--color-surface-2);
  color: var(--color-text-muted);
}

.nt-empty-ico svg { width: 22px; height: 22px; }
.nt-empty-title { margin: 12px 0 0; font-weight: 600; color: var(--color-text); }
.nt-empty-sub { margin: 2px 0 0; font-size: .8125rem; color: var(--color-text-muted); }

/* Modal: portal-card pads 1.5em, which would inset the header/footer bars, so
   .nt-modal zeroes it and the bars carry their own padding. */
.nt-modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 42rem;
  max-height: 90vh;
  padding: 0;
  overflow: hidden;
  box-shadow: var(--shadow-3);
}

.nt-modal:hover { box-shadow: var(--shadow-3); }

.nt-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--color-border-inner);
  background: var(--color-surface-2);
  flex: 0 0 auto;
}

.nt-modal-title { margin: 0; font-size: 1.0625rem; font-weight: 700; color: var(--color-text); }
.nt-modal-sub { margin: 0; font-size: .75rem; color: var(--color-text-muted); }
.nt-modal-body { padding: 20px; overflow-y: auto; }

.nt-modal-foot {
  display: flex;
  justify-content: flex-end;
  padding: 12px 20px;
  border-top: 1px solid var(--color-border-inner);
  background: var(--color-surface-2);
  flex: 0 0 auto;
}

@media (max-width: 640px) {
  .nt-card { padding: 12px; gap: 10px; }
  .nt-modal-body { padding: 14px; }
}

/* ==========================================================================
   SHARED CHROME HELPERS
   icon() emits a viewBox-only <svg>; an unsized replaced element defaults to
   300x150, so every glyph is sized either by the container rule above or by
   the macro's own size argument (the latter is what the sidebar footer uses,
   because a .portal-btn-* svg rule here would override the Tailwind sizes
   other pages already set on their button icons). Page-level glyphs use
   svg.gi below, which sizes them in `em` so no per-page rule is needed.
   ========================================================================== */
.exit-warn-ico { display: inline-flex; flex: 0 0 auto; color: var(--color-warning); }
.ob-ico { display: inline-flex; flex: 0 0 auto; color: var(--color-warning); }

/* ==========================================================================
   INLINE GLYPHS, STATUS DOTS AND ACTION BUTTONS
   The colour emoji that used to label page controls are now
   <svg class="gi"><use href="#ic-…"> refs into the sprite. Sizing in `em`
   means a glyph scales with the text it sits next to (text-xs → 12px,
   text-xl → 20px), so no page needs its own width/height rule — and an
   unsized sprite <svg> can never fall back to the 300x150 default.
   ========================================================================== */
svg.gi {
  /* Tailwind's preflight sets `svg { display: block }`, which put every glyph
     on its own line above the words it labels whenever the parent was not a
     flex container — page headings, tabs and inline labels all showed this. */
  display: inline-block;
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
  vertical-align: -0.16em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.gi-lg { width: 1.3em; height: 1.3em; }

/* Status circles (the old 🟢): colour comes from the theme tokens, so the
   dot reads correctly in both themes instead of a fixed green/red bitmap. */
.st-dot {
  display: inline-block;
  width: 0.55em;
  height: 0.55em;
  border-radius: 50%;
  background: var(--color-text-faint);
  vertical-align: 0.02em;
}
.st-dot--ok { background: var(--color-success); }
.st-dot--warn { background: var(--color-warning); }
.st-dot--bad { background: var(--color-danger); }
.st-dot--info { background: var(--color-primary-text); }

/* Action buttons. Radius and padding stay with the page (Tailwind classes) —
   this only fixes how an icon and a label sit together, and adds the press,
   focus and disabled feedback the emoji labels never had. */
.portal-btn-primary,
.portal-btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  white-space: nowrap;
  transition: background-color .15s ease, color .15s ease,
              box-shadow .15s ease, transform .08s ease;
}
.portal-btn-primary:active:not(:disabled),
.portal-btn-secondary:active:not(:disabled) { transform: translateY(1px); }
.portal-btn-primary:focus-visible,
.portal-btn-secondary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 38%, transparent);
}
.portal-btn-primary:disabled,
.portal-btn-secondary:disabled { opacity: .45; pointer-events: none; }

/* ==========================================================================
   ROW ACTIONS
   A table's action cell used to be three solid pills competing with the row's
   own data for attention. Here they shrink to one consistent size and go quiet
   — a hairline outline and muted text that only picks up colour on hover — so
   the eye reads the row first and the actions second. Descendant selectors,
   not `>`, because most cells already wrap their buttons in a Tailwind flex
   row; templates only add the `row-actions` class to whatever that row is.
   The padding/font-size below deliberately out-specifies the utilities pages
   set on individual buttons (text-xs py-1 px-2), which is what makes the
   action cells across the portal line up with each other.
   ========================================================================== */
.row-actions {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: nowrap;
  gap: 4px;
}
.row-actions .portal-btn-primary,
.row-actions .portal-btn-secondary,
.row-actions .portal-btn-accent,
.row-actions .portal-btn-danger,
.row-actions .portal-btn-success,
.row-actions .portal-btn-warning {
  padding: 4px 9px;
  font-size: .75rem;
  line-height: 1.35;
  border-radius: .35em;
  letter-spacing: .01em;
}
.row-actions .portal-btn-secondary {
  background-color: transparent;
  color: var(--color-text-muted);
  box-shadow: inset 0 0 0 1px var(--color-border);
}
.row-actions .portal-btn-secondary:hover {
  background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
  border-color: transparent;
  color: var(--color-primary-text);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 45%, transparent);
  opacity: 1;
}
.row-actions .portal-btn-danger {
  background-color: transparent;
  border-color: transparent;
  color: var(--color-danger);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-danger) 38%, transparent);
}
.row-actions .portal-btn-danger:hover {
  background-color: var(--color-danger);
  color: var(--color-on-danger, #ffffff);
  box-shadow: inset 0 0 0 1px var(--color-danger);
}
.row-actions .portal-btn-primary {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.row-actions .portal-btn-primary:hover {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18),
              0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent);
}

/* ==========================================================================
   QUICK ACCESS CARDS
   The dashboard module tiles. When the colour emoji were stripped from the
   tuples these cards lost their header entirely — an empty <span> — so the
   glyph now comes from the sprite via icon(). The chip sizes it with
   font-size (svg.gi is 1em wide), so the icon scales with the card's own text
   and no page needs a width/height rule. Layout lives here rather than in
   Tailwind classes so a card cannot end up half-styled on a page that forgot
   the utilities.
   ========================================================================== */
.qa-card {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  min-width: 0;
  padding: 0.875rem;
  border-radius: 0.5em;
  background-color: var(--color-surface);
  box-shadow: var(--shadow-1);
  color: inherit;
  text-decoration: none;
  transition: box-shadow .18s ease, transform .12s ease, background-color .18s ease;
}
.qa-card:hover {
  box-shadow: var(--shadow-2);
  background-color: var(--color-surface-hover);
  transform: translateY(-1px);
}
.qa-card:focus-visible {
  outline: none;
  box-shadow: var(--shadow-2),
              0 0 0 3px color-mix(in srgb, var(--color-primary) 30%, transparent);
}
.qa-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 0.55em;
  font-size: 1.0625rem;
  color: var(--color-primary-text);
  background-color: color-mix(in srgb, var(--color-primary) 14%, transparent);
}
.qa-card:hover .qa-ico { background-color: color-mix(in srgb, var(--color-primary) 22%, transparent); }
.qa-text {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
.qa-title {
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--color-text);
  overflow-wrap: break-word;
}
.qa-sub {
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--color-text-muted);
  overflow-wrap: break-word;
}

/* ==========================================================================
   FORM LAYER (partials/_form.html)
   The user CRUD forms were one long stack of full-width inputs with no
   grouping and no visible required affordance. .form-grid puts short fields
   two or three to a row when there is room and collapses to one column on a
   phone, without a breakpoint in the template.
   Error state goes on the wrapper (.fld--error) and never on the control:
   passing class= to a WTForms field REPLACES its render_kw class, which would
   strip the `portal-input` styling every field already carries.
   ========================================================================== */
.form-section + .form-section { margin-top: 1rem; }
.form-sec-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.15rem;
}
.form-sec-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 0.45em;
  font-size: 0.9375rem;
  color: var(--color-primary-text);
  background-color: color-mix(in srgb, var(--color-primary) 14%, transparent);
}
.form-sec-title {
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--color-text);
}
.form-sec-badge { margin-left: auto; flex: 0 0 auto; }
.form-sec-desc {
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--color-text-muted);
  margin-bottom: 0.9rem;
  overflow-wrap: break-word;
}
.form-section--warning { box-shadow: var(--shadow-1), inset 3px 0 0 var(--color-warning); }
.form-section--warning .form-sec-ico {
  color: var(--color-warning);
  background-color: color-mix(in srgb, var(--color-warning) 16%, transparent);
}

.form-grid {
  display: grid;
  gap: 0.875rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
.form-grid + .form-grid { margin-top: 0.875rem; }
.fld {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}
.fld-full { grid-column: 1 / -1; }
.fld-label {
  display: flex;
  align-items: center;
  gap: 0.3em;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text-muted);
}
.fld-req {
  color: var(--color-danger);
  font-weight: 700;
  line-height: 1;
}
.fld-hint,
.fld-error {
  font-size: 0.75rem;
  line-height: 1.4;
  overflow-wrap: break-word;
}
.fld-hint { color: var(--color-text-faint); }
.fld-error { color: var(--color-danger); font-weight: 500; }
.fld-error::before { content: "\2022\00a0"; }

/* The control keeps its own classes; only the ring changes. */
.fld--error .portal-input,
.fld--error textarea,
.fld--error select {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 22%, transparent);
}

/* Checkbox rows: the whole line is the tap target, not just the 13px box. */
.fld-box {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--color-text);
  cursor: pointer;
}
.portal-checkbox {
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  margin-top: 0.1rem;
  accent-color: var(--color-primary);
  cursor: pointer;
}

/* Native file pickers rendered raw looked like a foreign control: no token
   colours, no padding, an unthemeable button. The box model comes from the
   tokens; the ::file-selector-button covers Chromium/Firefox, the -webkit-
   pseudo-element covers Safari. */
.fld-file input[type="file"] {
  width: 100%;
  max-width: 100%;
  padding: 0.5rem;
  border: 1px dashed var(--color-border-input);
  border-radius: 0.375em;
  background-color: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  cursor: pointer;
}
.fld-file input[type="file"]:hover { border-color: var(--color-primary); }
.fld-file input[type="file"]::file-selector-button,
.fld-file input[type="file"]::-webkit-file-upload-button {
  margin-right: 0.6rem;
  padding: 0.3rem 0.7rem;
  border: 0;
  border-radius: 0.3em;
  background-color: var(--color-primary);
  color: var(--color-on-primary);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
}
.fld-file input[type="file"]:disabled { opacity: .55; cursor: not-allowed; }

/* Headshot row: avatar on the left, picker and "remove" on the right. The
   avatar is a fixed circle so the layout cannot jump when a photo loads. */
.fld-photo-row {
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
}
.fld-photo-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 9999px;
  overflow: hidden;
  font-weight: 700;
  color: var(--color-primary-text);
  background-color: color-mix(in srgb, var(--color-primary) 14%, transparent);
}
.fld-photo-img { display: block; width: 100%; height: 100%; object-fit: cover; }
.fld-photo-ph { font-size: 1.1rem; line-height: 1; }
.fld-photo-pick { flex: 1 1 auto; min-width: 0; gap: 0.4rem; }
.actions-note {
  margin-right: auto;
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--color-text-muted);
  overflow-wrap: break-word;
}

@keyframes sb-fade { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .sb,
  .sb * { transition: none !important; animation: none !important; }
}
