/* Application shell — slim top utility bar + fixed left navigation rail.
   Loaded only by the pages that mount PortalShell (the admin dashboard and the
   student portal), never by /admin/browse/, /admin/quiz/ or either callback
   page. Every rule is additionally gated on body.has-shell so that even if the
   stylesheet is loaded somewhere unexpected it changes nothing on its own.

   Why a separate file rather than additions to portal.css: admin/index.html
   loads /portal/portal.css, so anything added there silently reshapes the
   content browser and the quiz editor too. Keeping the shell here makes the
   admin-portal coupling explicit and leaves portal.css alone. */

body.has-shell {
  /* 16rem holds the longest admin label ("Training Courses" plus its caret) on
     one line, and is the widest rail the student table can afford beside it —
     see the breakpoint note below. */
  --app-util-h: 56px;
  --app-rail-w: 16rem;
}

/* ---------- skip link ----------
   The rail precedes <main> in the DOM, so keyboard users get a way past it. */
.app-skip {
  position: absolute;
  left: -9999px;
  z-index: 100;
  padding: .6rem 1rem;
  background: var(--paper, #fff);
  border: 1px solid var(--teal, #00897B);
  color: var(--ink, #1B2A4A);
  font: 600 .9rem var(--sans, 'Inter', sans-serif);
  text-decoration: none;
}
.app-skip:focus { left: .75rem; top: .5rem; }

/* ---------- slim top utility bar ----------
   What is left of the old horizontal navigation: the brand mark, who is signed
   in, and sign out. No section links — those moved to the rail. On the student
   portal this also replaces the public marketing header, which pointed out of
   the portal and duplicated chrome the signed-in pages do not need. */
.app-util {
  position: sticky;
  top: 0;
  z-index: 80;
  min-height: var(--app-util-h);
  background: rgba(255, 255, 255, .97);
  backdrop-filter: saturate(140%) blur(6px);
  border-bottom: 1px solid var(--line-cool, #E7EBF0);
}
.app-util-inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: var(--app-util-h);
  padding: .4rem 24px;
}
.app-brand { flex: none; text-decoration: none; }
.app-brand .b-name {
  display: block;
  font-family: var(--serif, 'Playfair Display', Georgia, serif);
  font-size: 1.02rem;
  font-weight: 600;
  color: var(--ink, #1B2A4A);
  line-height: 1.2;
}
.app-brand .b-sub {
  display: block;
  font: 500 .72rem var(--sans, 'Inter', sans-serif);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--teal, #00897B);
}
/* The account line keeps .signout-row and #signout because the existing sign-in
   proof scripts read that class and click that id — but .signout-row's 2rem top
   margin belongs to the in-flow variant, not to a bar, so it is reset here. */
.app-util .signout-row {
  margin: 0 0 0 auto;
  text-align: right;
  font-size: .84rem;
  color: var(--muted, #7c8798);
}
.app-util .signout-row a { color: var(--muted, #7c8798); text-decoration: underline; cursor: pointer; }
.app-util .signout-row a:hover { color: var(--teal, #00897B); }

/* Hamburger. Deliberately NOT the site's .nav-toggle: styles.css force-shows
   that one below 780px, which would fight this shell's 1180px breakpoint. */
.app-drawer-toggle {
  flex: none;
  appearance: none;
  background: none;
  border: 0;
  cursor: pointer;
  width: 44px;
  height: 44px;
  margin-left: -.6rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  padding: 0 10px;
}
.app-drawer-toggle span {
  display: block;
  height: 2px;
  background: var(--ink, #1B2A4A);
  transition: transform .2s ease, opacity .2s ease;
}
body.drawer-open .app-drawer-toggle span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
body.drawer-open .app-drawer-toggle span:nth-child(2) { opacity: 0; }
body.drawer-open .app-drawer-toggle span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ---------- the rail ----------
   Fixed to the viewport rather than made a flex child of .container, so the
   existing .container / .portal-card / .admin-dashboard-card{max-width:none}
   system is untouched — only the main column's left inset changes. */
.app-side {
  position: fixed;
  left: 0;
  /* Starts BELOW the utility bar at every width. The bar is z-index 80 and the
     rail 70, so a rail anchored at top:0 would slide its first nav item
     underneath the bar — visible but not clickable. Sitting below it also keeps
     the hamburger reachable while the drawer is open, so the control that
     opened it is the control that closes it. */
  top: var(--app-util-h);
  bottom: 0;
  z-index: 70;
  width: var(--app-rail-w);
  max-width: 86vw;
  overflow-y: auto;
  background: var(--paper, #fff);
  border-right: 1px solid var(--line-cool, #E7EBF0);
  transform: translateX(-100%);
  transition: transform .22s ease;
}
body.drawer-open .app-side { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .app-side { transition: none; }
  .app-drawer-toggle span { transition: none; }
}
.app-side-inner { padding: 1rem .6rem 2rem; }
.app-side-foot {
  margin-top: 1.2rem;
  padding: .9rem .95rem 0;
  border-top: 1px solid var(--line-cool, #E7EBF0);
}
.app-side-foot .portal-admin-strip {
  display: block;
  max-width: none;
  margin: 0;
  padding: .7rem .8rem;
  font-size: .84rem;
}
.app-side-foot .portal-admin-strip .btn { margin-top: .5rem; }

.app-scrim {
  position: fixed;
  inset: 0;
  z-index: 65;
  background: rgba(27, 42, 74, .38);
}
.app-scrim[hidden] { display: none; }

/* ---------- rail navigation: the admin dropdowns as nested lists ----------
   navHtml() emits exactly the markup it always did — only its parent element
   changed — so bindNav(), closeAllDrops() and the document-level listeners in
   admin.js keep working with no edit. All that changes here is the axis: the
   row of tabs becomes a column, and the absolute dropdown panel becomes the
   same static indented list the 720px rule already produced on phones. Hover
   (admin.css:180) and tap (.open, admin.css:181) both still reveal it. */
.app-side .admin-nav {
  flex-direction: column;
  flex-wrap: wrap;
  gap: 0;
  margin: 0;
  border-bottom: 0;
}
.app-side .admin-nav-item { width: 100%; position: static; }
.app-side .admin-nav-trigger {
  width: 100%;
  justify-content: flex-start;
  border-bottom: 0;
  border-left: 2px solid transparent;
  border-radius: 0 6px 6px 0;
  padding: .62rem .95rem;
}
.app-side .admin-nav-trigger:hover { background: #F6F9FA; }
.app-side .admin-nav-trigger .admin-nav-caret { margin-left: auto; }
.app-side .admin-nav-count { margin-left: .15rem; }
.app-side .admin-nav-item.active > .admin-nav-trigger {
  border-bottom-color: transparent;
  border-left-color: var(--teal, #00897B);
  background: var(--teal-soft, #E7F1EF);
}
.app-side .admin-nav-drop,
.app-side .admin-nav-drop.courses {
  position: static;
  min-width: 0;
  max-height: none;
  margin: .1rem 0 .35rem 1.6rem;
  padding: .1rem 0;
  background: none;
  border: 0;
  border-left: 2px solid var(--line-cool, #E7EBF0);
  border-radius: 0;
  box-shadow: none;
}
.app-side .admin-nav-opt {
  border-radius: 0 4px 4px 0;
  padding: .45rem .7rem;
  font-size: .86rem;
  white-space: normal;
}

/* In the rail the submenu expands IN FLOW, so hover-to-open would shove every
   item below it down the moment the pointer crossed a label — and shove them
   back up again as the pointer moved on, so the thing you were aiming at is no
   longer under the cursor. (This is not theoretical: it made the nav test's
   click land on the wrong element.) An absolute panel could hover safely; a
   vertical accordion cannot. Click and tap are therefore the single opening
   gesture at every width, which also makes desktop and touch behave alike.
   The selected section's own submenu is opened for you by openDropId, so the
   course filter is still one click away, exactly as before. */
.app-side .admin-nav-item:hover > .admin-nav-drop { display: none; }
.app-side .admin-nav-item.open > .admin-nav-drop { display: block; }

/* ---------- rail navigation: the student portal ----------
   Reuses .portal-nav / .portal-tab from portal.css; only the axis and the
   active marker move from a bottom underline to a left edge. */
.app-side .portal-nav {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  margin: 0;
  border-bottom: 0;
}
.app-side .portal-nav-item { display: flex; flex-direction: column; }
.app-side .portal-tab {
  padding: .62rem .95rem;
  border-bottom: 0;
  border-left: 2px solid transparent;
  border-radius: 0 6px 6px 0;
  margin-bottom: 0;
}
.app-side .portal-tab:hover { background: #F6F9FA; }
.app-side .portal-tab.active {
  border-bottom-color: transparent;
  border-left-color: var(--teal, #00897B);
  background: var(--teal-soft, #E7F1EF);
}
.portal-subnav {
  list-style: none;
  margin: .1rem 0 .35rem 1.6rem;
  padding: .1rem 0;
  border-left: 2px solid var(--line-cool, #E7EBF0);
}
.portal-subtab {
  display: block;
  padding: .42rem .7rem;
  font: 500 .86rem var(--sans, 'Inter', sans-serif);
  color: var(--body, #48566b);
  text-decoration: none;
  border-radius: 0 4px 4px 0;
}
.portal-subtab:hover { background: #F2F6F8; color: var(--teal, #00897B); }
.portal-subtab.active { color: var(--teal, #00897B); font-weight: 600; }

/* ---------- desktop: the rail becomes permanent ----------
   1280px, and the number is load-bearing — it was measured, not guessed. The
   six-column student table needs 882px once Initial Training's six module pips
   and the longest email are counted. Beside a 16rem rail that width is only
   available from about 1270px up (at 1180 it overflows by 88px and the
   Certificate column clips). 1280 is a standard laptop width, so every
   mainstream desktop gets the fixed rail; below it the shell is a drawer and
   every existing width assumption is exactly what it was before the rail
   existed. If this is ever lowered, re-measure — do not just change the number. */
@media (min-width: 1280px) {
  .app-drawer-toggle { display: none; }
  .app-scrim { display: none; }
  .app-side {
    z-index: 30;
    transform: none;
  }
  body.has-rail .portal-main,
  body.has-rail .site-footer { padding-left: var(--app-rail-w); }
  /* Reclaim the rail's width from cell padding so the widest table still fits
     without a scrollbar in the 1180-1400 band. */
  body.has-rail .admin-dashboard-card .admin-table th,
  body.has-rail .admin-dashboard-card .admin-table td { padding: .8rem .85rem; }
}

/* ---------- shared metric row ----------
   Moved here from admin.css (where it was dashboard-only) because the student
   dashboard rail now shows the same kind of figures and the portal does not
   load admin.css. Playfair numerals, Inter labels, no card boxes. */
.admin-stats { display: flex; flex-wrap: wrap; gap: 2.75rem; }
.admin-stat { display: flex; flex-direction: column; gap: .25rem; }
.admin-stat-num {
  font-family: var(--serif, 'Playfair Display', Georgia, serif);
  font-size: 2.15rem;
  font-weight: 600;
  line-height: 1;
  color: var(--ink, #1B2A4A);
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
.admin-stat-label { font: 500 .82rem var(--sans, 'Inter', sans-serif); color: var(--body, #48566b); }

/* A fourth metric joins the row, so the fixed 2.75rem gap becomes elastic —
   otherwise the last stat wraps alone on mid-width screens. */
.admin-summary .admin-stats { gap: 1rem clamp(1.3rem, 2.6vw, 2.75rem); }

/* ---------- shared activity feed ----------
   A hairline list, not a card: the same visual weight as .admin-scope, so it
   reads as context under the table rather than as a second dashboard. */
.admin-activity { margin: 1.6rem 0 0; max-width: 46rem; }
.admin-activity-title,
.portal-rail-title {
  font: 600 .78rem var(--sans, 'Inter', sans-serif);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted, #7c8798);
  margin: 0 0 .4rem;
}
.admin-feed { list-style: none; margin: 0; padding: 0; }
.admin-feed-row {
  display: flex;
  align-items: baseline;
  gap: .7rem;
  flex-wrap: wrap;
  padding: .62rem 0;
  border-bottom: 1px solid #F0F3F6;
  font-size: .92rem;
  color: var(--ink, #1B2A4A);
}
.admin-feed-row:last-child { border-bottom: 0; }
.admin-feed-dot {
  flex: none;
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: #D8DFE6;
  align-self: center;
}
/* Same categorical meaning as the chips in the tables above: teal for the
   outcomes that count, amber for a failed attempt, neutral for everything else. */
.admin-feed-dot.cert,
.admin-feed-dot.pass { background: var(--teal, #00897B); }
.admin-feed-dot.fail { background: #E7A94B; }
.admin-feed-main { flex: 1; min-width: 13rem; }
.admin-feed-meta {
  margin-left: auto;
  font-size: .82rem;
  color: var(--muted, #7c8798);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Status pill. Moved here from admin.css so the student certificate page can
   show the same pass/fail marker the admin tables use — one vocabulary across
   both surfaces rather than two that nearly match. */
.chip {
  display: inline-block;
  padding: .2rem .6rem;
  border-radius: 999px;
  font-size: .78rem;
  font-weight: 600;
  background: #F0F3F6;
  color: var(--body, #48566b);
  white-space: nowrap;
}
.chip.pass { background: #E1F2EF; color: #00695C; }
.chip.warn { background: #FBF0E4; color: #9A6B2F; }

/* Assessment history under a certificate — the attempts the payload always
   carried but which only ever showed as a single line of hint text. */
.cert-history { margin-top: 1.3rem; padding-top: 1.1rem; border-top: 1px solid var(--line-cool, #E7EBF0); }
.cert-history .admin-feed-row:first-child { padding-top: .2rem; }

/* ---------- student dashboard: cards + progress rail ----------
   The portal container is 1140px wide but every .portal-card is capped at
   44rem, so roughly 436px of every page was permanently empty. The rail fills
   exactly that gutter with the student's own figures — all of it already in the
   get-progress payload, so nothing new is fetched. Below the breakpoint it
   stacks under the cards rather than disappearing. */
.portal-shelf { display: block; }
.portal-rail { margin-top: 1.8rem; }
.portal-rail-block + .portal-rail-block {
  margin-top: 1.4rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line-cool, #E7EBF0);
}
.portal-rail .admin-stats { gap: 1.2rem 2rem; }
.portal-rail .admin-stat-num { font-size: 1.7rem; }
.portal-rail-next {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  text-decoration: none;
  color: var(--ink, #1B2A4A);
  font: 600 .95rem var(--sans, 'Inter', sans-serif);
  line-height: 1.45;
}
.portal-rail-next:hover { color: var(--teal, #00897B); }
.portal-rail-go { margin-left: auto; color: var(--muted, #7c8798); }
.portal-rail-sub { font: 400 .84rem var(--sans, 'Inter', sans-serif); color: var(--muted, #7c8798); margin: .2rem 0 0; }
.portal-rail .admin-feed-row { font-size: .88rem; }
.portal-rail .admin-feed-meta { margin-left: 0; width: 100%; }

@media (min-width: 1280px) {
  .portal-shelf {
    display: grid;
    grid-template-columns: minmax(0, 44rem) minmax(14rem, 1fr);
    gap: 2.4rem;
    align-items: start;
  }
  .portal-rail { margin-top: 0; }
  /* The cards already carry their own 44rem cap; inside the grid column they
     should simply fill it. */
  .portal-shelf-main > .portal-card { max-width: none; }
}

/* ---------- small screens ---------- */
@media (max-width: 720px) {
  .app-util-inner { gap: .6rem; padding: .4rem 16px; }
  .app-brand .b-name { font-size: .95rem; }
  .app-util .signout-row { font-size: .78rem; }
  /* On a phone the address is the least useful half of the account line and
     the first thing to cost a second row — keep the action, drop the label. */
  .app-util-who { display: none; }
  /* Compact the metric row on phones. These live here rather than in admin.css
     because shell.css loads after it and therefore owns .admin-stat* now. */
  .admin-summary .admin-stats { gap: .9rem 1.5rem; }
  .admin-stat-num { font-size: 1.8rem; }
  .admin-activity { margin-top: 1.2rem; }
  .admin-feed-meta { margin-left: 0; width: 100%; white-space: normal; }
}
