/* ============================================================
   layout.css — frame: sidebar + header + main + footer
   Phase 3: real logo integration + tighter responsive tuning
   ============================================================ */

/* App frame */
/* GH #253: the frame is pinned to the viewport height and clips overflow so
   .app-header / .reader-strip / .app-footer (siblings of .app-main below)
   stay fixed in place; only .app-main scrolls internally. */
.app-frame {
  margin-left: var(--sidebar-w);
  /* #253: was min-height (grew past one viewport, forcing the whole body to
     scroll and dragging .app-header/.app-footer off-screen with it). Fixed
     to exactly one viewport tall instead so header/footer -- both normal-flow
     children of this column -- can never leave the viewport; .app-main (the
     one flex:1 child) becomes the sole scrolling region for page content. */
  height: 100vh;
  /* Account for iOS safe area when running as installed PWA -- box-sizing:
     border-box (base.css) means this padding is carved out of the fixed
     height above rather than growing past it. */
  padding-bottom: env(safe-area-inset-bottom);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: margin-left var(--t-med) ease;
}

/* ---- Sidebar ---- */
.sidebar {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: var(--sidebar-w);
  background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
  border-right: 1px solid var(--border-1);
  display: flex;
  flex-direction: column;
  z-index: 50;
  /* GH #432: width was unanimated (only transform was), while .app-frame's
     margin-left (which also derives from --sidebar-w) transitions over the
     same var(--t-med) window. Toggling body.sidebar-collapsed therefore
     snapped this box straight to its new width while the main content was
     still easing into its new offset, so the two visibly overlapped/gapped
     for the length of the transition. Animate width in lockstep so the
     sidebar box and the content margin it drives always agree. */
  transition: transform var(--t-med) ease, width var(--t-med) ease;
  /* iOS notch / Dynamic Island safe area */
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: var(--sidebar-brand-pad-y) 16px;
  border-bottom: 1px solid var(--border-1);
}
.brand-logo {
  width: var(--brand-logo-size); height: var(--brand-logo-size);
  flex-shrink: 0;
  border-radius: var(--radius-1);
  object-fit: contain;
  display: block;
  background: #000;
  padding: 2px;
  /* Subtle outer glow that makes the logo pop on the dark sidebar */
  box-shadow: 0 0 0 1px var(--border-2), 0 4px 12px rgba(251, 191, 36, 0.08);
}
.brand-text { min-width: 0; }
.brand-name {
  font-weight: 700;
  color: var(--accent-amber);
  font-size: 13px;
  letter-spacing: 0.08em;
}
.brand-sub {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* GH #255: static search/filter box above the nav list. */
.sidebar-search-wrap {
  padding: 10px 14px 0;
  flex-shrink: 0;
}
/* GH #517: wrapper positions the magnifying-glass icon inside the input
   without a JS-driven layout -- pure CSS, matching the field's static
   markup in index.html. */
.sidebar-search-field { position: relative; }
.sidebar-search-icon {
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-3);
  pointer-events: none;
}
.sidebar-search-input {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 10px 7px 28px;
  border-radius: var(--radius-1);
  border: 1px solid var(--border-1);
  background: var(--bg-3);
  color: var(--text-1);
  font-size: 12.5px;
}
.sidebar-search-input::placeholder { color: var(--text-3); }
.sidebar-search-input:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 2px var(--accent-cyan-dim);
}
/* GH #362: one global control to expand/expand-collapse every sidebar
   module section at once, instead of clicking each of the 7 headers. */
.sidebar-toggle-all {
  width: 100%;
  margin-top: 6px;
  padding: 5px 8px;
  border-radius: var(--radius-1);
  border: 1px solid var(--border-1);
  background: var(--bg-3);
  color: var(--text-3);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.sidebar-toggle-all:hover, .sidebar-toggle-all:focus-visible { color: var(--text-1); background: var(--bg-2); }
.sidebar-toggle-all:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; }
body.sidebar-collapsed .sidebar-search-wrap { display: none; }
.nav-no-results {
  padding: 10px 12px;
  font-size: 12px;
  color: var(--text-3);
  font-style: italic;
}
.nav-item.nav-hidden, .nav-section.nav-hidden { display: none !important; }
/* GH #438/#534: API Documentation entry, hidden ONLY for a user who lacks
   the "system:api_docs" permission (initApiDocsNavAvailability(), main.js).
   Kept separate from .nav-hidden above -- that class is re-toggled per
   keystroke by the sidebar search filter and would un-hide this entry the
   moment its label matched. No longer set for environment/schema-probe
   reasons (GH #534) -- a permitted user always sees the entry. */
.nav-item.nav-env-disabled { display: none !important; }

.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 12px 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  -webkit-overflow-scrolling: touch;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  /* GH #629: extra left padding (12px -> 24px) indents every sub-item
     relative to its .nav-section-title (still 12px), so the Main Module ->
     Sub Module hierarchy reads visually nested, not two same-level lists.
     Overridden back to a flush 0/12px by the collapsed icon-rail and Top
     nav-layout rules further down (both target `.nav-item` at equal-or-
     higher specificity and come later in the file), so neither the
     icon-only rail nor the horizontal dropdown menu is affected. */
  padding: 9px 12px 9px 24px;
  border-radius: var(--radius-1);
  color: var(--text-3);
  font-size: 13px;
  font-weight: 500;
  transition: background var(--t-fast), color var(--t-fast);
  min-height: 36px;
}
.nav-item:hover { background: var(--bg-3); color: var(--text-1); }
.nav-item.is-active {
  background: var(--accent-cyan-dim);
  color: var(--accent-cyan);
  box-shadow: inset 2px 0 0 var(--accent-cyan);
}
.nav-icon {
  width: 22px;
  text-align: center;
  font-size: 14px;
  opacity: 0.85;
  font-family: var(--font-sans);
  flex-shrink: 0;
}

/* Grouped navigation: logical sections with a small muted heading. */
.nav-section {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.nav-section + .nav-section { margin-top: 10px; }
.nav-section-title {
  /* GH #351: now a <button>, not a <div> -- reset the native button chrome
     (border/background/font/full-width text-align) back to how the label
     looked before, then add the expand/collapse affordance on top.

     Round 425 (GH #526): this used to be `justify-content: space-between`
     across its 3 flex items (the ::before icon, the .nav-section-label
     text, the ::after chevron) -- with a wide sidebar and a short label,
     that distributes the row's large leftover space in two roughly-equal
     gaps around the label, dragging it away from its icon toward the
     middle of the row instead of sitting flush beside it. `.nav-item` (the
     submenu rows immediately below) has no `justify-content` at all
     (defaults to flex-start), so its icon+label always hug the left edge --
     the mismatch between the two is exactly what read as "main menu text
     is center-aligned, inconsistent with the submenu". Dropped
     `space-between`; the chevron gets its own `margin-left: auto` below to
     stay pinned to the right edge, so the row's ONLY remaining flexible gap
     is between the label and the chevron, not between the icon and label. */
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  border: none;
  background: none;
  font: inherit;
  cursor: pointer;
  padding: 6px 12px 4px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  opacity: 0.7;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav-section-title:hover, .nav-section-title:focus-visible { opacity: 1; color: var(--text-1); }
.nav-section-title:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; }
/* GH #519: a per-module icon, rendered from the `data-icon` attribute (not
   real DOM text) precisely so it never touches this button's textContent --
   the sidebar search filter and the accordion's persisted collapse-state
   key both key off it (main.js). Shown alongside the label when expanded;
   the sole visible thing once the sidebar rail collapses to icon-only,
   below. */
.nav-section-title::before {
  content: attr(data-icon);
  /* GH #629: same fixed 22px column as .nav-item's own .nav-icon (above),
     centered the same way -- so a module's header icon and its sub-items'
     icons are consistently sized/aligned as one icon system, not two
     independently-sized ones that happened to use similar emoji. */
  display: inline-block;
  width: 22px;
  flex-shrink: 0;
  font-size: 12.5px;
  line-height: 1;
  text-align: center;
  text-transform: none; /* the title's own uppercase transform must not touch emoji glyphs */
}
.nav-section-title::after {
  content: "▾";
  flex-shrink: 0;
  font-size: 9px;
  margin-left: auto; /* GH #526: pin the chevron to the right edge now that
                         the row's justify-content no longer does it */
  transition: transform var(--t-fast);
}
.nav-section.collapsed .nav-section-title::after { transform: rotate(-90deg); }
/* Accordion: collapsed modules hide their items -- except while the sidebar
   search box has an active query (all matches must stay reachable). GH #367:
   the Top nav layout used to be a third exception here (no clickable title
   existed, so items always had to show) -- now that Top nav has its own
   clickable per-module title and dropdown (layout.css:~634), it follows the
   same collapsed/expanded rule as every other layout instead of being
   force-shown.
   GH #632: the icon-only collapsed rail used to be a THIRD exception too
   (see the removed `body.sidebar-collapsed .nav-section.collapsed .nav-item`
   half of this selector), on the theory that with no title label to click
   and re-expand, items had to force-show to stay reachable at all. But
   accordion-EXPANDED sections were never covered by this rule either way --
   their items just fall through to this block's own default `.nav-item`
   display, unaffected by `.collapsed` -- so the rail ended up rendering
   EVERY module's submenu icons at once regardless of accordion state,
   defeating the point of collapsing to icons-only. GH #527's hover fly-out
   (below) is the real replacement reachability path now, so the force-show
   exception is gone; see `body.sidebar-collapsed .nav-section .nav-item` in
   the desktop breakpoint block (~line 658) for the rail-wide hide that now
   covers both accordion states uniformly. */
.nav-section.collapsed .nav-item { display: none; }
body.nav-search-active .nav-section.collapsed .nav-item {
  display: flex;
}

/* GH #527: hover/focus fly-out for a module header in the icon-only
   collapsed rail -- appended to `document.body` (main.js), not nested
   inside `.sidebar-nav`, since that container's `overflow-y: auto` forces
   `overflow-x` to also compute as `auto` (CSS overflow spec), clipping any
   absolutely-positioned descendant. `position: fixed` here escapes that
   entirely; main.js computes left/top from the hovered header's own
   `getBoundingClientRect()`. Same visual language as the pre-existing
   `.export-menu-list` dropdown (pages.css) for a consistent "floating menu"
   look across the app. */
.sidebar-flyout {
  position: fixed;
  z-index: 300;
  min-width: 180px;
  max-width: 260px;
  max-height: calc(100vh - 16px);
  overflow-y: auto;
  padding: 6px;
  background: var(--bg-2, #141414);
  border: 1px solid var(--border-1, var(--border, #5a5a5a));
  border-radius: var(--radius-1, 8px);
  box-shadow: var(--shadow-panel, 0 8px 24px rgba(0,0,0,0.35));
}
.sidebar-flyout[hidden] { display: none; }
.sidebar-flyout-title {
  padding: 4px 10px 6px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  opacity: 0.7;
  white-space: nowrap;
}
.sidebar-flyout-item {
  white-space: nowrap;
}
.sidebar-flyout-item.is-active {
  background: var(--accent-cyan-dim);
  color: var(--accent-cyan);
}

.sidebar-foot {
  border-top: 1px solid var(--border-1);
  padding: 12px 16px;
  font-size: 11.5px;
  color: var(--text-3);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.conn-indicator { display: flex; align-items: center; gap: 8px; }
.pwa-install-btn { width: 100%; justify-content: center; }

/* ---- Header ---- */
.app-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  padding: 18px 28px 12px;
  flex-wrap: wrap;
  padding-right: max(28px, env(safe-area-inset-right));
}
.header-titles { min-width: 0; flex: 1; }
.header-title-row {
  display: flex; align-items: center; gap: 14px;
  min-width: 0;
}
.header-logo {
  width: 52px; height: 52px;
  flex-shrink: 0;
  border-radius: var(--radius-1);
  object-fit: contain;
  background: #000;
  padding: 2px;
  box-shadow: 0 0 0 1px var(--border-1);
}
.header-title {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  color: var(--text-1);
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.header-subtitle {
  margin: 4px 0 0;
  font-size: 12.5px;
  color: var(--text-3);
}
.header-meta {
  display: flex;
  /* GH #574: was flex-start, top-aligning the single-line header-tool
     buttons (search/favorites/create-new/theme, header_tools.js) against
     the two-line clock-block/notif-bell beside them -- the short buttons
     floated above their own visual centre, reading as misaligned/
     "empty space" next to the taller blocks. Centering the whole row
     keeps every header control on one visual baseline regardless of how
     many lines its own content wraps to. */
  align-items: center;
  /* GH #583: was 28px, reading as excessive/unbalanced gaps between the
     Search/Favorites/Create New/Theme tool blocks and the notif-bell/clock
     blocks. 12px matches .reader-strip's own block-to-block gap
     (layout.css, .reader-strip) -- the closest existing analog, a row of
     multiple distinct labeled blocks rather than bare buttons. */
  gap: 12px;
  flex-wrap: wrap;
}
.meta-block { min-width: 0; }
.meta-label {
  display: block;
  font-size: 11px;
  color: var(--text-3);
  margin-bottom: 4px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.meta-line {
  font-size: 12.5px;
  color: var(--text-3);
  margin: 2px 0;
  display: flex; align-items: center; gap: 6px;
}
.meta-line .meta-label {
  display: inline; margin: 0; text-transform: none; letter-spacing: 0;
  font-size: 12.5px; color: var(--text-3);
}
.meta-line span:not(.meta-label) { color: var(--text-2); }

.icon-btn {
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: var(--radius-1);
  color: var(--text-3);
  background: var(--bg-2);
  border: 1px solid var(--border-2);
  transition: color var(--t-fast), background var(--t-fast);
  flex-shrink: 0;
}
.icon-btn:hover { color: var(--accent-cyan); background: var(--bg-3); }

/* ---- Reader strip ---- */
.reader-strip {
  margin: 4px 28px 16px;
  padding: 10px 16px;
  border: 1px solid var(--border-1);
  border-radius: var(--radius-2);
  background: var(--bg-1);
  display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap;
  font-size: 13px;
}
.strip-item { display: flex; align-items: center; gap: 8px; }
.strip-label { color: var(--text-3); }
.strip-strong { color: var(--text-1); font-weight: 600; }
.strip-mono { font-family: var(--font-mono); color: var(--accent-cyan); }
.strip-sep { color: var(--text-4); }

/* ---- Main view-mount ---- */
/* #253: this is now the ONLY scrolling region in the app frame -- min-height:
   0 is required so this flex:1 child can actually shrink to its allotted
   space inside the fixed-height .app-frame instead of growing to fit its
   content (the usual flexbox "min-height:auto" trap), which is what lets
   overflow-y:auto actually engage here instead of pushing .app-frame taller. */
.app-main {
  flex: 1;
  min-height: 0;
  padding: 0 28px 28px;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* ---- Footer ---- */
/* GH #628: standardized Infotronicx footer -- logo + "Designed & Developed
   by" pinned left, copyright + live version pinned right, via
   justify-content instead of the old .foot-grow spacer (removed, no longer
   needed with the two-group flex layout below). Same var(--text-*)/
   var(--bg-1) tokens as before so it stays correct in both themes without
   any extra dark/light overrides. */
.app-footer {
  margin-top: auto;
  padding: 14px 28px;
  border-top: 1px solid var(--border-1);
  background: var(--bg-1);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 12px;
  color: var(--text-3);
  flex-wrap: wrap;
}
.foot-left, .foot-right {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.foot-logo { border-radius: 4px; flex-shrink: 0; display: block; }
.foot-sep { color: var(--text-4); }
.foot-copy { color: var(--accent-cyan); }
.foot-version strong { color: var(--text-1); }
@media (max-width: 640px) {
  /* Narrow viewports: stack the two groups instead of wrapping mid-group,
     which used to interleave the logo/copy/version awkwardly. */
  .app-footer { flex-direction: column; align-items: flex-start; gap: 6px; }
}

/* ---- Sidebar toggle (mobile) ---- */
.sidebar-toggle {
  position: fixed;
  /* GH #320: the button is position:fixed to the viewport, independent of
     .app-header's own flex flow, so it must match the header's metrics by
     hand. .app-header's padding-top is max(18px, env(safe-area-inset-top))
     (layout.css:394-397) and .header-logo is 40px tall at this breakpoint
     (layout.css:399) -- both had drifted from this button's 16px/40px,
     leaving it ~2px higher than the logo/title row it sits beside. */
  top: max(18px, env(safe-area-inset-top));
  left: max(16px, env(safe-area-inset-left));
  z-index: 100;
  width: 40px; height: 40px;
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  background: var(--bg-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-1);
  /* GH #432: the desktop collapsed-state rule below moves this button via
     `left: calc(...var(--sidebar-w)...)`; without a transition it jumped
     instantly to its new spot ahead of the still-animating sidebar/content,
     compounding the same-cause overlap fixed on .sidebar above. */
  transition: left var(--t-med) ease;
}
.sidebar-toggle span {
  display: block;
  width: 18px; height: 2px;
  background: var(--text-1);
  border-radius: 1px;
  transition: transform var(--t-fast), opacity var(--t-fast);
}
.sidebar-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.sidebar-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.sidebar-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ---- Offline banner ---- */
.offline-banner {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--accent-amber);
  color: #1a1102;
  font-size: 12.5px;
  font-weight: 600;
  z-index: 200;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  animation: slideUp var(--t-med) ease;
}
@keyframes slideUp {
  from { opacity: 0; transform: translate(-50%, 12px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* ============================================================ Responsive ============================================================ */

@media (max-width: 1280px) {
  .header-title { font-size: 19px; }
  .header-meta { gap: 10px; }
}

@media (max-width: 1100px) {
  :root { --sidebar-w: 200px; --brand-logo-size: 40px; }
  .header-logo { width: 46px; height: 46px; }
  .header-title { font-size: 18px; }
  .header-meta { gap: 9px; }
}

@media (max-width: 820px) {
  .sidebar {
    transform: translateX(-100%);
    width: 280px;
    box-shadow: 12px 0 32px rgba(0, 0, 0, 0.6);
  }
  .sidebar.is-open { transform: translateX(0); }
  .app-frame { margin-left: 0; }
  .sidebar-toggle { display: flex; }

  .app-header {
    padding-left: 68px;
    padding-right: 18px;
    padding-top: max(18px, env(safe-area-inset-top));
  }
  .header-logo { width: 40px; height: 40px; }
  .header-title { font-size: 17px; line-height: 1.25; }
  .header-subtitle { font-size: 11.5px; }

  .reader-strip {
    margin-left: 18px; margin-right: 18px;
    padding: 8px 12px;
    font-size: 12px;
  }
  .app-main { padding: 0 18px 18px; }
  .app-footer { padding: 12px 18px; font-size: 11px; }

  .header-meta { width: 100%; gap: 8px; }
}

@media (max-width: 480px) {
  .header-title { font-size: 15px; }
  .header-subtitle { font-size: 11px; }
  .header-logo { width: 36px; height: 36px; }
  /* GH #320: shrink in lockstep with .header-logo (above) so the toggle
     stays centered on the same row instead of standing 4px taller than the
     logo it's aligned next to. top offset is unchanged -- both still start
     at .app-header's padding-top. */
  .sidebar-toggle { width: 36px; height: 36px; }
  .reader-strip { font-size: 11.5px; gap: 8px; }
  .strip-sep { display: none; }
  .meta-block.clock-block { display: none; }
  .app-footer { font-size: 10.5px; }
  .foot-sep { display: none; }
}

@media (max-width: 820px) and (orientation: landscape) and (max-height: 500px) {
  .app-header { padding-top: 10px; padding-bottom: 6px; }
  .header-subtitle { display: none; }
  .reader-strip { margin-top: 0; margin-bottom: 10px; }
}

@media (display-mode: standalone) {
  .app-header { padding-top: 22px; }
}

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

/* ===== Desktop sidebar collapse (persistent) ===== */
/* GH #320 (round 2): this used to start at min-width:1025px while the mobile
   drawer toggle (max-width:820px, ~line 390) stopped at 820px, leaving an
   821px-1024px "tablet"/mid-size gap where .sidebar-toggle matched NEITHER
   media query and fell back to the base rule's `display: none` -- the
   hamburger vanished entirely at those widths (confirmed via computed
   styles: display:"none" for every probed width 821-1024, "flex" again at
   1025+), even though the sidebar itself already renders in its full
   non-drawer desktop form there (no translateX, .app-frame keeps its
   sidebar-width margin-left). That's the gap QA's ~874px-wide screenshot
   fell into -- lowered to 821px so it picks up exactly where the mobile
   rule leaves off, with no gap and no overlap. */
@media (min-width: 821px) {
  .sidebar-toggle {
    /* QA #2: collapse control sits at the top-left of the sidebar, to the LEFT
       of the brand logo (☰ | logo | title) — no overlap with the logo.
       GH #320 (round 3): rounds 1 and 2 each hand-copied a pixel guess
       (top/width/height) that was never actually derived from
       .brand-logo's real box, so it drifted out of alignment again as
       soon as either one changed. top/width/height now read the SAME
       --sidebar-brand-pad-y/--brand-logo-size custom properties that
       .sidebar-brand and .brand-logo themselves use (base.css), so the
       toggle is always exactly the same size and vertical position as the
       logo it sits beside, by construction, at every breakpoint. */
    display: flex;
    top: var(--sidebar-brand-pad-y); bottom: auto; left: 14px;
    width: var(--brand-logo-size); height: var(--brand-logo-size);
    transition: left var(--t-med) ease;
  }
  /* Reserve space at the left of the brand so the logo starts to the right
     of ☰, scaling with the toggle's own (logo-derived) width instead of a
     hardcoded guess that stops matching once --brand-logo-size changes. */
  .sidebar-brand { padding-left: calc(14px + var(--brand-logo-size) + 8px); }
  body.sidebar-collapsed { --sidebar-w: 66px; }
  body.sidebar-collapsed .sidebar { overflow-x: hidden; }
  body.sidebar-collapsed .nav-item { justify-content: center; gap: 0; padding: 9px 0; }
  /* GH #37: keep ONLY the icon in the collapsed rail. The previous rule
     hid span:last-child, but a licence-locked item appends a .nav-lock
     badge as the last child — so the label span survived and the module
     NAME showed instead of just the icon. Hide every non-icon span. */
  body.sidebar-collapsed .nav-item > span:not(.nav-icon) { display: none; }
  /* GH #632: the rail must show ONLY the main module icons
     (.nav-section-title, styled below) -- every .nav-item (submenu icon) it
     contains stays hidden regardless of that module's accordion open/closed
     state. Scoped to `.nav-section .nav-item` so it never touches the
     GH #527 hover fly-out's own `.sidebar-flyout-item` copies, which reuse
     the `.nav-item` class but live outside `.nav-section` (appended to
     `document.body`, see main.js) -- that's still the one place submenu
     icons/labels surface while the rail is collapsed. */
  body.sidebar-collapsed .nav-section .nav-item { display: none; }
  /* GH #527: the fly-out's own items reuse the `.nav-item` class purely for
     its existing hover/icon styling -- but that's also exactly what the two
     rules right above this one target, at the SAME specificity (one class
     on `body`, one class on the item), so being appended to `document.body`
     (main.js's portal pattern) doesn't exempt them: without a MORE specific
     override here, whichever of these two same-specificity rule pairs is
     later in the file wins, hiding the fly-out's whole reason for existing
     (its real text labels) and re-centering its icons as if it were just
     another slice of the collapsed rail. `.sidebar-flyout-item` on top of
     `.sidebar-flyout` gives this pair one more class match than the plain
     `.nav-item` rules above, so it always wins regardless of source order. */
  body.sidebar-collapsed .sidebar-flyout .sidebar-flyout-item {
    justify-content: flex-start;
    gap: 12px;
    padding: 9px 12px;
  }
  body.sidebar-collapsed .sidebar-flyout .sidebar-flyout-item > span:not(.nav-icon) {
    display: inline;
  }
  /* GH #519: previously `display: none` dropped the group heading (and its
     icon) entirely in the collapsed rail, leaving only bare sub-item icons
     with no way to tell which module they belonged to. Keep the heading's
     own icon (::before, above) visible and centered, same as a collapsed
     .nav-item; hide just the text label and the expand/collapse chevron so
     the rail stays exactly as compact as before. The button is made
     non-interactive here (main.js guards the click handler) -- toggling a
     module's accordion state is meaningless while the rail is collapsed,
     since every item stays hidden regardless of it either way (GH #632:
     `body.sidebar-collapsed .nav-section .nav-item` above hides the whole
     module's items uniformly, whether its accordion is open or closed), so
     a click here would only silently change persisted state with no
     visible effect. */
  body.sidebar-collapsed .nav-section-title {
    justify-content: center;
    padding: 6px 0 4px;
    cursor: default;
  }
  body.sidebar-collapsed .nav-section-title .nav-section-label,
  body.sidebar-collapsed .nav-section-title::after {
    display: none;
  }
  body.sidebar-collapsed .nav-section + .nav-section { margin-top: 4px; }
  /* GH #255: the search box is meaningless in the icon-only rail — hide it. */
  body.sidebar-collapsed .nav-search { display: none; }
  /* GH #37: the brand text is .brand-text (divs, not spans), so the old
     `.sidebar-brand span` selector never hid it and it overflowed the
     66px rail, overlapping the collapse control. Hide the text block. */
  body.sidebar-collapsed .sidebar-brand .brand-text { display: none; }
  /* GH #634: `.sidebar-foot` used to be hidden outright in the collapsed
     rail, taking the Install App / Add-to-Home-Screen button down with it
     -- an important, otherwise-unreachable action (installing the PWA) with
     no equivalent control anywhere else in the app. Keep the foot itself
     visible. (Its identity chip, `#user-menu`, was the other reason this
     rule used to also hide that chip specifically in the collapsed rail --
     GH #641 removed the chip from index.html entirely, so that sub-rule is
     gone too; the button's own icon-only treatment is below.) */
  body.sidebar-collapsed .sidebar-foot { padding-left: 0; padding-right: 0; }
  body.sidebar-collapsed .pwa-install-btn {
    width: auto;
    align-self: center;
    padding: 9px;
  }
  /* Icon-only in the rail; the button's `title="Install App"` attribute
     (index.html) supplies the native tooltip GH #634 asks for. */
  body.sidebar-collapsed .pwa-install-btn .pwa-install-label { display: none; }
  /* Collapsed: only the ☰ control shows (centered); logo hidden to fit 66px. */
  body.sidebar-collapsed .sidebar-brand { justify-content: center; padding-left: 0; }
  body.sidebar-collapsed .brand-logo { display: none; }
  /* GH #467: hiding BOTH .brand-logo and .brand-text (immediately above)
     left .sidebar-brand with no visible content at all, so its height
     collapsed to just its own top/bottom padding (2 * --sidebar-brand-pad-y
     = 28px) instead of the ~72px it has in the expanded state (padding +
     the 44px logo that sets the row's real height there). .sidebar-nav is
     .sidebar-brand's very next sibling in normal flow, so it rode up to
     start at ~29px -- but .sidebar-toggle is position:fixed, sized/placed
     to match the EXPANDED row (top: --sidebar-brand-pad-y, height:
     --brand-logo-size => bottom ~58px) and does NOT participate in that
     flow at all. The result: the toggle button's bottom ~29px physically
     overlapped the first 1-2 collapsed nav items, clipping/obscuring their
     icons under the hamburger control (reproduced live: Dashboard's icon
     and active-state highlight sat directly under the ☰ box). Reserve the
     SAME height the row has when the logo is visible, derived from the same
     custom properties the toggle itself uses, so the row never shrinks out
     from under the fixed control it hosts. */
  body.sidebar-collapsed .sidebar-brand {
    min-height: calc(var(--brand-logo-size) + 2 * var(--sidebar-brand-pad-y));
  }
  /* GH #358: the previous fixed `left: 16px` was a hand-picked guess (like
     the ones GH #320 rounds 1-2 made and had to fix), so it drifted out of
     center whenever --brand-logo-size changed across breakpoints, leaving
     the icon lopsided/clipped in the 66px collapsed rail. Center it by
     construction instead, the same way GH #320 round 3 fixed the expanded
     state: derive left from the actual collapsed rail width and toggle
     size rather than a guessed pixel offset. */
  body.sidebar-collapsed .sidebar-toggle { left: calc((var(--sidebar-w) - var(--brand-logo-size)) / 2); }
}

/* ===== Restart-required banner (QA #13/#15/#21) ===== */
.restart-banner { display:flex; align-items:center; justify-content:space-between; gap:12px;
  flex-wrap:wrap; padding:10px 18px; background:rgba(245,158,11,0.14);
  border-bottom:1px solid rgba(245,158,11,0.5); color:var(--text); font-size:13px; }
.restart-banner[hidden] { display:none; }
.restart-banner-msg { display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
.restart-banner-ico { font-size:16px; }
/* GH #291: collapsed by default so a config save touching many settings
   doesn't dump the whole field list inline and balloon the banner's height. */
.restart-banner-details { flex-basis:100%; }
.restart-banner-details summary { cursor:pointer; color:var(--accent-amber, #f59e0b); font-size:12px; }
.restart-banner-details[open] summary { margin-bottom:4px; }
.restart-banner-fields { display:block; font-size:12px; color:var(--text-muted, #94a3b8); }
.restart-banner-actions { display:flex; gap:8px; }
.restart-banner-actions .btn-secondary, .restart-banner-actions .btn-primary { padding:5px 12px; font-size:12px; }
/* GH #8/#17: live restart-progress states inside the banner */
.restart-progress { display:flex; align-items:center; gap:10px; }
.restart-progress-ico { font-size:16px; }
.restart-progress-ico.spin { display:inline-block; animation:restart-spin 1s linear infinite; }
@keyframes restart-spin { to { transform:rotate(360deg); } }
.restart-progress-ok  { color:var(--accent-green, #22c55e); }
.restart-progress-err { color:var(--accent-red, #f87171); }

/* ============================================================
   QA #1 — Global navigation layout: Left (default) / Right / Top.
   Controlled by <html data-nav-layout="left|right|top"> (themes.js),
   set from Settings → Appearance and persisted per account.
   ============================================================ */

/* ---- RIGHT: sidebar mirrored to the right edge ---- */
/* GH #243: the content frame itself (.app-frame) carries its own
   margin-left reserving space for the LEFT sidebar (see the base
   .app-frame rule above); overriding body's margin alone left that
   reservation in place, so switching to Right Sidebar produced a
   sidebar-width gap on the left with nothing in it. Override .app-frame
   directly instead. */
html[data-nav-layout="right"] .app-frame { margin-left: 0; margin-right: var(--sidebar-w); }
html[data-nav-layout="right"] .sidebar {
  left: auto; right: 0;
  border-right: none;
  border-left: 1px solid var(--border-1);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
/* GH #320 (round 2): kept in lockstep with the base persistent-toggle
   breakpoint above (now 821px, was 1025px) so Right-sidebar layout doesn't
   reopen the same 821-1024px gap. */
@media (min-width: 821px) {
  html[data-nav-layout="right"] .sidebar-toggle { left: auto; right: 14px; }
  html[data-nav-layout="right"] .sidebar-brand { padding-left: 16px; padding-right: 56px; }
  html[data-nav-layout="right"] body.sidebar-collapsed .sidebar-brand { padding-right: 0; }
  /* GH #358 (reopened): this was a hand-picked `right: 16px` guess -- the
     same class of bug the Left/default layout's collapsed toggle already
     had fixed above (body.sidebar-collapsed .sidebar-toggle) -- so it never
     centered within the collapsed 66px rail and drifted out of alignment
     whenever --sidebar-w/--brand-logo-size changed. Derive it the same way:
     centered by construction from the actual rail/icon sizes, not a guess. */
  html[data-nav-layout="right"] body.sidebar-collapsed .sidebar-toggle {
    right: calc((var(--sidebar-w) - var(--brand-logo-size)) / 2); left: auto;
  }
}

/* ---- TOP: sidebar becomes a horizontal top navigation bar ---- */
/* GH #444: --topnav-h used to be a fixed 58px baked into both the single
   row's own height AND the offset .app-frame reserves for it. On any
   >=1025px viewport too narrow to fit every module title on one line (a
   laptop-width browser window, not just a phone/tablet — GH #437 already
   covers the <=1024px drawer fallback), the row had nowhere to grow and no
   scroll/overflow affordance, so trailing titles rendered past the header
   edge and were visually cut off/unreachable, exactly as reported. Split
   the fixed per-row height (--topnav-row-h, never changes) from the bar's
   *total* height (--topnav-h, kept in sync with the real rendered height by
   initTopNavAutoHeight() in main.js) so the row can wrap onto additional
   lines instead of overflowing, while .app-frame's offset below always
   matches however tall the wrapped bar actually is. */
html[data-nav-layout="top"] { --topnav-row-h: 58px; --topnav-h: 58px; }
/* GH #244: same root cause as #243 — .app-frame's own margin-left (not
   body's) reserves the sidebar column, so it needs the override, plus a
   margin-top for the now-horizontal bar instead of a left column.
   GH #253: that margin-top pushes .app-frame down by --topnav-h without
   shrinking it, so its fixed 100vh height (see the base .app-frame rule)
   would run --topnav-h past the bottom of the viewport, forcing document
   scroll again in this nav layout specifically — shrink the height by the
   same amount the top navbar pushed it down. */
html[data-nav-layout="top"] .app-frame {
  margin-left: 0; margin-top: var(--topnav-h);
  height: calc(100vh - var(--topnav-h));
}
html[data-nav-layout="top"] .sidebar {
  top: 0; left: 0; right: 0; bottom: auto;
  width: 100%; min-height: var(--topnav-row-h); height: auto;
  flex-direction: row; flex-wrap: wrap; align-items: stretch;
  border-right: none; border-bottom: 1px solid var(--border-1);
  /* GH #367: dropdowns (`.nav-section-items`) render below this bar --
     `overflow-x: auto` + `overflow-y: hidden` (the old flattened-strip
     design's horizontal scroll) would clip them (mixing a scrolling axis
     with `visible` computes the visible axis to `auto` too per the CSS2.1
     overflow rule, so a plain `overflow-y: visible` here wouldn't actually
     stay visible). `overflow: visible` trades away horizontal auto-scroll
     for letting the bar wrap onto more than one line (GH #444) when titles
     alone (not the full flattened item list) don't all fit on one. */
  overflow: visible;
}
html[data-nav-layout="top"] .sidebar-brand {
  border-bottom: none; border-right: 1px solid var(--border-1);
  padding: 8px 18px; flex-shrink: 0; align-items: center;
}
html[data-nav-layout="top"] { --brand-logo-size: 34px; }
html[data-nav-layout="top"] .sidebar-nav {
  /* GH #444: wrap onto additional rows instead of overflowing past the
     header edge when the module titles don't all fit on one line. */
  flex-direction: row; flex-wrap: wrap; align-items: stretch;
  overflow: visible;
  flex: 1 1 auto; padding: 0 6px; gap: 2px;
}
/* GH #367: the top nav bar used to flatten every module's full item list
   into one permanently-visible horizontal strip (see the removed
   `nav-section-title { display: none }` rule below in history) -- crowded,
   and every submenu item stayed on screen even for modules the operator
   wasn't using. Only the main module titles show by default now; clicking
   one reveals JUST its own submenu as a dropdown panel underneath, reusing
   the existing per-section `.collapsed` class/state machine
   (initSidebarAccordion() in main.js) that already drives the left/right
   sidebar accordion (GH #351/#362) -- main.js additionally makes Top mode's
   dropdowns mutually exclusive and closes them after a click, whereas the
   sidebar allows several modules expanded at once. */
html[data-nav-layout="top"] .nav-section {
  position: relative;
  flex-direction: row; align-items: stretch;
}
html[data-nav-layout="top"] .nav-section + .nav-section {
  margin-top: 0; margin-left: 8px; padding-left: 8px;
  border-left: 1px solid var(--border-1);
}
html[data-nav-layout="top"] .nav-section-title {
  display: flex; align-items: center;
  /* GH #444: the fixed per-row height, not --topnav-h (now the bar's total,
     possibly-wrapped height) — a title must stay one row tall regardless of
     how many rows the bar as a whole wraps onto. */
  height: var(--topnav-row-h);
  padding: 0 12px; margin: 0;
  white-space: nowrap; flex-shrink: 0;
  background: none; border: none; border-bottom: 2px solid transparent;
  font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-3); cursor: pointer;
}
html[data-nav-layout="top"] .nav-section-title::after {
  content: "▾"; margin-left: 6px; font-size: 9px; opacity: 0.7;
}
html[data-nav-layout="top"] .nav-section:not(.collapsed) .nav-section-title {
  color: var(--text-1); border-bottom-color: var(--accent-cyan, #06b6d4);
}
/* Submenu dropdown: the `.nav-section-items` wrapper (added at runtime by
   initSidebarAccordion(), main.js) is what's actually positioned -- doing
   it on the wrapper rather than each `.nav-item` means the panel's width
   never affects `.nav-section`'s own box in the horizontal bar, and items
   stack normally (flex column) inside it instead of needing a fragile
   per-child offset hack. */
html[data-nav-layout="top"] .nav-section.collapsed .nav-section-items { display: none; }
html[data-nav-layout="top"] .nav-section-items {
  position: absolute; top: 100%; left: 0;
  display: flex; flex-direction: column;
  min-width: 220px; z-index: 60;
  background: var(--bg-1); border: 1px solid var(--border-1);
  border-radius: 0 0 var(--radius-1) var(--radius-1);
  box-shadow: var(--shadow-panel);
  padding: 6px;
}
html[data-nav-layout="top"] .nav-item {
  border-left: none; border-bottom: none; border-radius: var(--radius-1);
  padding: 8px 12px;
}
html[data-nav-layout="top"] .nav-item.active {
  border-left: none; background: var(--bg-2); color: var(--text-1);
}
html[data-nav-layout="top"] .sidebar-foot { display: none; }
html[data-nav-layout="top"] .sidebar-search-wrap { display: none; }
/* GH #437: this used to hide the toggle from 821px up, matching the OLD
   820px drawer-fallback breakpoint below -- but main.js's own click handler
   (initSidebarToggle) has always treated <=1024px as "mobile drawer" via
   window.matchMedia("(max-width: 1024px)"). CSS never matched that JS
   threshold, so from 821-1024px (tablet/small-laptop) Top nav's title row
   (position:visible, no wrap, no scroll -- see the unconditional
   .sidebar-nav rule above) had nowhere to go but overflow past the header,
   AND no hamburger was shown to reach the hidden items either. Raised to
   1025px so the toggle now stays visible exactly through the range JS
   already treats as "drawer". */
@media (min-width: 1025px) {
  html[data-nav-layout="top"] .sidebar-toggle { display: none; }
}
/* On narrow/tablet screens the Top layout gracefully falls back to the
   mobile drawer. GH #437: raised from 820px to 1024px (see comment above)
   so the horizontal title row never has an unhandled width range to
   overflow in -- it now either fits (>=1025px, full row) or is a drawer
   (<=1024px), with no gap between the two. Right-layout's own fallback
   (never reported broken) is left at its original 820px. */
@media (max-width: 1024px) {
  html[data-nav-layout="top"] .app-frame { margin-top: 0; margin-right: 0; }
  /* #253: the top navbar collapses into the normal mobile drawer below this
     breakpoint (margin-top reset above), so .app-frame no longer needs the
     calc() shrink -- give it back the full viewport like every other layout. */
  html[data-nav-layout="top"] .app-frame { height: 100vh; }
  html[data-nav-layout="top"] .sidebar {
    height: auto; width: min(84vw, 320px); flex-direction: column;
    left: 0; right: auto; border-bottom: none; border-right: 1px solid var(--border-1);
    /* GH #437: the generic slide-in-drawer transform (base .sidebar rule,
       max-width:820px above) doesn't reach this wider 821-1024px range --
       without its own transform here the column above would render as a
       permanently-open panel overlapping the page instead of an off-canvas
       drawer toggled by .sidebar-toggle/.is-open. */
    transform: translateX(-100%);
    box-shadow: 12px 0 32px rgba(0, 0, 0, 0.6);
  }
  html[data-nav-layout="top"] .sidebar.is-open { transform: translateX(0); }
  html[data-nav-layout="top"] .sidebar-nav { flex-direction: column; }
  /* GH #367: the desktop dropdown styling (row layout, absolutely
     positioned `.nav-section-items` panel) doesn't apply once Top nav has
     collapsed into the normal mobile drawer above -- revert to the same
     plain vertical accordion the drawer uses for every other layout. */
  html[data-nav-layout="top"] .nav-section { flex-direction: column; }
  html[data-nav-layout="top"] .nav-section-title { height: auto; border-bottom: none; }
  html[data-nav-layout="top"] .nav-section-items {
    position: static; box-shadow: none; border: none; background: none;
    padding: 0; min-width: 0;
  }
  html[data-nav-layout="top"] .nav-section.collapsed .nav-section-items { display: none; }
}
@media (max-width: 820px) {
  html[data-nav-layout="right"] .app-frame { margin-top: 0; margin-right: 0; }
  html[data-nav-layout="right"] .sidebar {
    height: auto; width: min(84vw, 320px); flex-direction: column;
    left: 0; right: auto; border-bottom: none; border-right: 1px solid var(--border-1);
  }
  html[data-nav-layout="right"] .sidebar-nav { flex-direction: column; }
}
