/*
 * Portal icon rail — the app's left-hand portal navigation (ISH-253).
 *
 * This is the single source of truth for the rail's appearance. It replaced the
 * per-page copies of the old top-right portal dropdown's CSS, which had drifted
 * across the 18 pages that carried it (see .docs/AGENT_GUIDE.md §N). The markup
 * this styles is rendered by renderPortalRail() in js/shared.js — don't hand-write
 * rail markup in a page, and don't re-declare these rules in a page's inline
 * <style>.
 *
 * Colors resolve against each page's own :root tokens where those exist, with
 * literal fallbacks so the rail still renders correctly on a page whose :root is
 * incomplete. Rail-specific tokens are declared on .portal-rail itself rather
 * than :root, so a page's inline <style> can't clobber them by redefining :root
 * after this file loads.
 *
 * Requires <body class="has-portal-rail"> for the 60px content offset. That class
 * is set statically in each page's HTML rather than by JS, so the layout doesn't
 * shift once scripts run. A page whose own `.top-nav` is not the usual
 * `z-index: 100` adds `has-high-top-nav` beside it — see the ISH-1076 block below.
 */

/* Content offset. Applied to body rather than each page's own content container
 * because those container classes differ per page (.page, .container, etc.);
 * padding on body shifts every in-flow child uniformly, including the sticky
 * .top-nav. The class selector also outranks the `* { padding: 0 }` reset that
 * most pages declare inline. */
body.has-portal-rail {
  padding-left: 60px;
}

.portal-rail {
  --rail-bg: #ffffff;
  --rail-icon: var(--text-secondary, #4b5563);
  --rail-icon-active: #ffffff;
  --rail-hover-bg: rgba(15, 23, 42, 0.06);
  --rail-active-bg: var(--red, #a62626);
  --rail-active-bg-hover: var(--red-hover, #8f2121);

  /* ISH-1048. Collapsed and expanded widths. Declared here rather than on
   * :root for the same reason as the colours above — a page's inline <style>
   * cannot clobber them by redefining :root after this file loads. */
  --rail-width: 60px;
  --rail-width-open: 210px;

  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--rail-width);
  background: var(--rail-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* ISH-1048: the 9px side padding is what pins the icon column in place while
   * the rail widens. Collapsed the content box is 60 - 18 = 42px, exactly one
   * .rail-item; expanded it is 210 - 18 = 192px and the item's fixed 42px icon
   * slot stays flush left. Either way the icon's centre sits at 9 + 21 = 30px,
   * so expanding reveals labels without shifting a single glyph. Changing this
   * padding moves the icons — re-measure both states if you do. */
  padding: 0.75rem 9px 0.6rem;
  overflow: hidden;
  transition: width 0.18s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 110;
  /* Neutral grey, deliberately not red-tinted — a red separation shadow was
   * tried during design review and rejected. */
  box-shadow:
    3px 0 16px rgba(15, 23, 42, 0.08),
    1px 0 0 rgba(15, 23, 42, 0.12);
}

/* ── Expand on hover (ISH-1048) ───────────────────────────────────────────────
 *
 * The rail OVERLAYS the page rather than pushing it: `position: fixed` above
 * plus `body.has-portal-rail { padding-left: 60px }` is a static offset, so
 * widening reflows nothing. That is the whole reason this is safe to do on
 * hover — a rail that pushed content would make every page jump.
 *
 * `:focus-within` is not decoration. Without it the rail is a hover-only
 * affordance and a keyboard user tabbing through the portals never sees a
 * label at all.
 *
 * ⚠️ `z-index: 110` stays. Lowering the rail under the top nav (z-index 100) to
 * stop the expanded panel covering the nav bar was tried and is worse: page
 * content is opaque and paints straight over the rail, eating the labels. The
 * accepted consequence is that an open rail covers the left of the nav bar,
 * including the portal name ISH-1048 adds there — the two cues hand off, since
 * the expanded rail names the current portal more loudly than the nav does.
 *
 * `:has(.rail-account.is-open)` keeps the rail expanded for as long as the account popover is
 * open, independent of hover/focus. This has to be its own trigger because the popover
 * (`#portal-menu`) is now portaled to a direct child of <body> — see js/shared.js's
 * renderPortalRail() — so hovering or clicking inside it no longer counts as hovering or
 * focusing *within* `.portal-rail`. Without this, the rail would snap back to 60px the instant
 * the pointer or focus left it for the now-detached menu, stranding the menu at the expanded
 * offset it was positioned against.
 */
.portal-rail:hover,
.portal-rail:focus-within,
.portal-rail:has(.rail-account.is-open) {
  width: var(--rail-width-open);
}

/* ── Pages whose own nav sits in a high z-index band (ISH-1076) ──────────────
 *
 * The block above assumes `.top-nav` is the `z-index: 100` every page declares.
 * Two were not: analytics.html and map.html both put it at 1500, so the nav
 * painted over the top ~52px of the rail and the expanded rail lost its whole
 * header row — the logo's brand text never appeared, and the hand-off ISH-1048
 * describes ran backwards.
 *
 * ISH-1076 took analytics.html's nav back to 100, where its 1500 was only ever
 * standing in for a stacking context its own dropdowns lacked. map.html's 1500
 * is load-bearing and stays: Leaflet's control panes reach 400 and the map's
 * floating UI climbs to 1002, so a nav at 100 would sit under the map. That
 * page opts in here instead.
 *
 * ⚠️ This is a modifier and not a change to the rail's own 110, which stays for
 * the reason above AND for a second one: `.modal-overlay` is z-index 1000-1080
 * on about a dozen pages, so a rail raised globally would paint over their
 * modals — a worse bug than the one being fixed. 1600 is chosen against
 * map.html's own stack: above its 1500 nav, below its 2000 modal overlay, so
 * modals still cover the rail there. A page adding this class needs the same
 * two numbers checked, or it trades this bug for the modal one.
 */
body.has-high-top-nav .portal-rail {
  z-index: 1600;
}

/* ── The rail's header row (ISH-1055) ────────────────────────────────────────
 *
 * The logo, and — while the rail is expanded — the app's name with "Portals"
 * under it, BESIDE the logo rather than beneath it. That placement is the whole
 * point: horizontal space only exists while the rail is open, so the caption
 * costs nothing at rest.
 *
 * ⚠️ ISH-1048 put that caption in its own row above the group and hid it with
 * `opacity: 0`, which hides an element WITHOUT REMOVING IT FROM LAYOUT. A
 * collapsed rail therefore reserved a full text row, and the logo-to-first-icon
 * gap went from 14.4px to ~32.8px on every page — visible as a misalignment,
 * with nothing on screen to explain it. Its own comment claimed it "costs
 * nothing at rest", which is the belief to distrust: `opacity` is paint, not
 * layout.
 *
 * ⚠️ Do NOT instead collapse that caption's height when the rail is closed. The
 * icons would then be pushed DOWN on hover, trading this bug for a worse one —
 * the expand block above exists to keep the icons still.
 *
 * `margin-bottom: 0.9rem` moved here off `.rail-logo`, so the gap below the
 * header is exactly the value it was before ISH-1048. The row's height is set by
 * the logo's 38px; the two stacked brand lines total ~28px, so nothing moves
 * vertically in either state.
 */
.rail-header {
  display: flex;
  align-items: center;
  align-self: stretch;
  margin-bottom: 0.9rem;
}

.rail-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 8px;
  overflow: hidden;
  flex-shrink: 0;
  margin-left: 2px;
}

/* Puts this text on the same column as the portal labels below it — 9 rail
 * padding + 2 logo margin + 38 logo + 8 = 57px, matching 9 + 42 icon slot + 6
 * slot margin. Change either gap and this one moves with it, or the open rail
 * grows a second left edge. */
.rail-brand {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  margin-left: 8px;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.12s ease 0.04s;
}

.portal-rail:hover .rail-brand,
.portal-rail:focus-within .rail-brand,
.portal-rail:has(.rail-account.is-open) .rail-brand {
  opacity: 1;
}

.rail-brand-name {
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--text, #0f172a);
}

/* What the icons below it are — the job ISH-1048's .rail-section-caption was
 * added to do, done without occupying a row of its own. */
.rail-brand-sub {
  font-size: 0.625rem;
  font-weight: 700;
  line-height: 1.25;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-label, #94a3b8);
}

.rail-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.rail-group {
  display: flex;
  flex-direction: column;
  /* `stretch`, not `center` (ISH-1048): an item has to be able to fill the
   * expanded rail. Centring it would re-centre the 42px collapsed item too and
   * break the fixed icon column the rail's own padding sets up. */
  align-items: stretch;
  width: 100%;
}

.rail-item {
  position: relative;
  /* ISH-1048: was `width: 42px; justify-content: center`. The item is now a row
   * — a fixed 42px icon slot, then the label — so it can grow with the rail
   * while the icon stays put. Collapsed, `overflow: hidden` on .portal-rail
   * clips everything past the slot. */
  width: auto;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  border-radius: 9px;
  color: var(--rail-icon);
  text-decoration: none;
  cursor: pointer;
  margin-bottom: 0.3rem;
  transition:
    background 0.15s ease,
    color 0.15s ease;
  border: none;
  background: none;
  font: inherit;
}

/* ISH-1352. An admin-only portal's anchor, before loadAccessContext() has said whether this viewer
 * may see it. renderPortalRail() always writes the anchor — it runs before the role is known and
 * nothing downstream can create one that was omitted — so this class is what keeps it out of sight
 * until applyPortalRailGates() either drops the class or removes the element.
 *
 * ⚠️ `display: none`, not `opacity: 0` or `visibility: hidden`. The rail is a flex COLUMN with
 * `margin-bottom` on every item, so a merely-invisible anchor still reserves a 42px row plus its
 * gap — the exact defect ISH-1055 fixed on the rail header's own caption. It must leave layout,
 * not just sight.
 *
 * ⚠️ Deliberately NOT keyed on the `hidden` attribute. `.rail-item` above declares `display: flex`,
 * which beats `[hidden]`'s user-agent `display: none` on specificity, and this repo has shipped
 * that bug enough times that it reads as "the page will not load" rather than as a style slip. */
.rail-item-gated {
  display: none;
}

.rail-item svg {
  width: 19px;
  height: 19px;
  display: block;
}

/* Neutral grey hover, deliberately not red-tinted — same design-review call as
 * the box-shadow above. */
.rail-item:hover {
  background: var(--rail-hover-bg);
  color: var(--text, #0f172a);
}

.rail-item.is-current {
  background: var(--rail-active-bg);
  color: var(--rail-icon-active);
}

.rail-item.is-current:hover {
  background: var(--rail-active-bg-hover);
  color: var(--rail-icon-active);
}

.rail-item:focus-visible {
  outline: 2px solid var(--red, #a62626);
  outline-offset: 2px;
}

.rail-spacer {
  flex: 1;
}

/* `width: auto` + `align-self: stretch` rather than a fixed 26px (ISH-1048): the
 * divider spans whatever the rail currently is, so it grows with the expansion
 * instead of sitting as a stub in the middle of an open rail. */
.rail-divider {
  width: auto;
  align-self: stretch;
  height: 1px;
  background: var(--border-light, #f1f5f9);
  margin: 0.5rem 0;
}

.rail-account-btn {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  /* `flex-start`, not `center` (ISH-1048): the button holds an icon slot and a
   * label now, so its contents must start at the icon column like every portal
   * row rather than being centred as a lone icon once was. */
  justify-content: flex-start;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
  padding: 0;
  border-radius: inherit;
}

/* The fixed-width cell the icon lives in. 42px matches the collapsed rail's
 * content box exactly, which is what keeps the icon's centre at 30px in both
 * states. `flex: 0 0 42px` — it must neither grow into the label's space nor
 * shrink when a long label arrives.
 *
 * The margin sets the gap between an icon and its label, and it goes HERE rather
 * than on .rail-label so the portal rows and the account button get it from one
 * rule. Without it the spacing is visibly lopsided: the 19px glyph sits centred
 * in a 42px cell, so there is 20.5px of air before the icon and only 11.5px
 * after it, which reads as the label crowding the icon. 6px evens that up.
 *
 * ⚠️ Margin, not a wider slot. Widening the slot would move the icon and break
 * the 30px centre the expand behaviour depends on. */
.rail-icon-slot {
  flex: 0 0 42px;
  margin-right: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The portal's name, revealed only while the rail is open. Opacity rather than
 * display/visibility so it can fade with the width rather than snapping in at
 * the end, and so it stays out of the accessibility tree's way either side —
 * the anchor keeps its own aria-label regardless. */
.rail-label {
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.12s ease 0.04s;
}

.portal-rail:hover .rail-label,
.portal-rail:focus-within .rail-label,
.portal-rail:has(.rail-account.is-open) .rail-label {
  opacity: 1;
}

/* The account row is outside .rail-group, so it needs the same left alignment
 * the group gets from `align-items: stretch`. (.rail-logo's equivalent lives on
 * .rail-header now — ISH-1055.) */
.rail-account {
  display: flex;
  align-items: center;
  /* ⚠️ Load-bearing, and its absence broke BOTH rail states.
   *
   * .rail-account is a direct child of .portal-rail, which sets
   * `align-items: center` — unlike the portal rows, which get
   * `align-items: stretch` from .rail-group. Without this the row shrinks to fit
   * its contents and centres:
   *
   *   expanded — it sits indented from the portal rows above it, so the rail has
   *     two left edges instead of one.
   *   collapsed — its width is the 42px icon slot PLUS the "Account" label, which
   *     is `opacity: 0` but still laid out (~92px). Centred in a 42px content box
   *     that drags the icon ~25px left, into .portal-rail's `overflow: hidden`.
   *     The icon renders clipped and off-column.
   *
   * .rail-divider below has always carried this; the account row was missed. */
  align-self: stretch;
}

/* ── The rail names a portal ONE way: `.rail-label` (ISH-1064) ───────────────
 *
 * A second mechanism used to live here: a dark hover flyout, absolutely
 * positioned at `left: calc(100% + 10px)` off each `.rail-item`. It is gone,
 * and it should not come back as a "collapsed-rail fallback", because it never
 * rendered in either state. `.portal-rail`'s `overflow: hidden` (see that rule)
 * clipped it exactly as it clipped the account popover — measured 202px inside
 * a 210px expanded rail and 61px inside a 60px collapsed one, entirely outside
 * the clip box both times. The rail's own comments and `.docs/AGENT_GUIDE.md`
 * asserted for two releases that it was "still the only label a collapsed rail
 * shows". It showed nothing, and nobody noticed, because hovering or focusing
 * the rail expands it and `.rail-label` says the same word.
 *
 * Removing it also removed the only two rules that existed to manage the
 * collision between the two mechanisms — an `opacity: 0 !important` suppression
 * on rail hover (ISH-1048, whose `!important` was needed purely to beat an
 * equally-specific later rule) and an older one keyed off `.rail-account
 * .is-open`. That is the point: one naming mechanism needs no suppression
 * rules, so a future change to the rail's hover behaviour has one fewer
 * invisible interaction to get right.
 *
 * If a collapsed-rail affordance is ever genuinely wanted, it cannot be an
 * absolutely-positioned child of `.portal-rail`. Portal it to <body> and
 * position it fixed, the way `.rail-account-menu` below now does.
 */

/*
 * Account popover.
 *
 * These .nav-portal-menu / .portal-menu-* / .portal-user-* rules were previously
 * duplicated in all 18 pages' inline <style> blocks; they live here now so the
 * popover has one definition. .rail-account-menu repositions the panel to fly out
 * from the rail's bottom-left instead of hanging below a top-right button.
 */
.nav-portal-menu {
  position: absolute;
  min-width: 260px;
  background: var(--card-bg, #ffffff);
  border: 1px solid var(--border, #e2e8f0);
  border-radius: var(--radius-lg, 12px);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
  padding: 0.35rem 0 0.5rem;
  z-index: 200;
}

.nav-portal-menu[hidden] {
  display: none;
}

/*
 * Portaled to a direct child of <body> (js/shared.js's renderPortalRail()), not nested inside
 * `.rail-account`, so it can escape `.portal-rail`'s stacking context (see that rule's own
 * comment). `position: absolute` from `.nav-portal-menu` above only worked when this panel had
 * an ancestor to be absolute *against*; `position: fixed` here plus the inline `left`/`bottom`
 * wirePortalRailAccountMenu() sets on every open (mirroring #shared-info-tooltip's
 * getBoundingClientRect() pattern, css/info-tooltip.css) replace that.
 *
 * z-index clears every value this repo declares below the Settings modal's 5000 — the highest
 * observed today is `.export-sidebar`'s 4001 (see js/settings-modal-z-index.test.js's ISH-622
 * sweep) — while staying under the modal itself and the toast layer (9999), the same ladder the
 * Settings modal clears this exact chrome for. Before this, the popover's own `z-index: 200`
 * (inherited from `.nav-portal-menu`) was trapped inside `.portal-rail`'s stacking context and
 * could never win against any of it, regardless of its own value.
 */
.rail-account-menu {
  position: fixed;
  top: auto;
  right: auto;
  z-index: 4900;
}

.portal-menu-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem;
  border-bottom: 1px solid var(--border-light, #f1f5f9);
}

.portal-user-avatar {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: var(--red, #a62626);
  color: #ffffff;
  font-size: 0.875rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  letter-spacing: 0.02em;
}

.portal-user-meta {
  min-width: 0;
}

.portal-user-name {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text, #0f172a);
  line-height: 1.3;
}

.portal-user-title {
  font-size: 0.8125rem;
  color: var(--text-secondary, #4b5563);
  margin-top: 0.15rem;
  line-height: 1.3;
}

.portal-menu-section-title {
  padding: 0.65rem 1rem 0.3rem;
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-label, #94a3b8);
}

.portal-menu-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  padding: 0.6rem 1rem;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text, #0f172a);
  text-decoration: none;
  background: none;
  border: none;
  cursor: pointer;
}

.portal-menu-item:hover {
  background: var(--page-bg, #f8f9fa);
}

.portal-menu-item.is-danger {
  color: var(--red, #a62626);
}

.portal-menu-divider {
  height: 1px;
  background: var(--border-light, #f1f5f9);
  margin: 0.35rem 0;
}

@media (prefers-reduced-motion: reduce) {
  .rail-item,
  /* ISH-1048 — the rail still expands, it just stops animating the width and
     the label fade. */
  .portal-rail,
  .rail-label,
  .rail-brand {
    transition: none;
  }
}
