:root {
  /* Site fonts (issue #255): Oswald for headings, card titles and the big
     stat numbers; Source Sans 3 for everything else — body text, buttons,
     inputs, labels. Loaded from index.html's <link>, not an @import here, so
     the request starts as soon as the HTML does rather than waiting on this
     file to arrive first. */
  --font-heading: 'Oswald', Impact, sans-serif;
  --font-body: 'Source Sans 3', system-ui, sans-serif;
  /* Header wordmark only (issue #317) — the closest free match to the brush
     script of the old "Rogue" logo image it replaces. */
  --font-script: 'Lobster', cursive;

  /* Black / white / grey only, in that order of priority:
     black = main + all backgrounds, white = emphasis/outline, grey = secondary. */
  --bg: #000000;
  --panel: #000000;
  --panel-2: #111111;     /* near-black elevation for inputs / tracks */
  --text: #ffffff;        /* white: primary text + emphasis */
  --muted: #9a9a9a;       /* grey: secondary text */
  --accent: #ffffff;      /* white: focus/hover outlines */
  --accent-2: #ffffff;
  --good: #ffffff;        /* progress / completion shown in white */
  --danger: #d0d0d0;      /* greyscale "danger" */
  --border: #777777;      /* grey outline */
  --border-strong: #ffffff;

  /* Rogue red (issue #265), used for: the active-nav bar, progress-bar fill,
     completed-checkbox fill, the quiz-passed badge, and the keyboard focus
     outline. Issue #271 adds exactly one more small red touch per page — a
     line, a tick, an icon, a hover glow — never a large fill. Everything
     else on the site stays black/white/grey. */
  --accent-red: #FF0000;

  /* Line weights (issue #16). A line's boldness on a black page is its width
     times its contrast, so this scale is only half the job — brightening
     --border from #333333 to #777777 is the other half. Every border, divider
     and outline in the app pulls from these four, so the whole site re-weights
     from here instead of from ~80 scattered pixel values.

     The tier is about what the line encloses, not how big that thing is:
       --line-sm      a line drawn INSIDE something already outlined (row
                      dividers in lists and tables, menu separators), or around
                      something small enough that 3px would swallow it (pills,
                      badges, chips, progress-bar tracks).
       --line         the default. Anything you read or click as one unit:
                      buttons, cards, rows, inputs, tiles, list containers.
       --line-xl      divisions between parts of the page: the top bar, the
                      sidebar, section rules, the footer.
       --line-accent  stripes whose whole job is to out-weigh the lines around
                      them, so this has to stay clear of --line-xl. */
  --line-sm: 2px;
  --line: 3px;
  --line-xl: 4px;
  --line-accent: 6px;

  /* Height of the locked top bar, border included: 10px padding twice, the
     50px .brand, and the --line-xl bottom border. The bar is sticky, so it
     covers the top --topbar-h of the viewport at every scroll position, and
     anything else pinned to the top of the screen has to start below it —
     the fixed sidebar, the view-as banner, the module-reader's fixed side
     list. Those all used hand-counted offsets (60px, 72px, 0) that no longer
     agreed with each other or with the bar. Measure from here instead.
     Overridden at the 720px breakpoint, where .brand shrinks to 40px. */
  --topbar-h: 70px;

  /* Height of the view-as banner, 0 when it isn't showing. app.js measures
     the banner's real rendered height (it varies with the viewed user's name
     and can wrap) and sets this via style.setProperty in renderViewAsBanner(),
     so anything pinned below --topbar-h — .sidebar, .module-side — can add
     this in to clear the banner too, instead of being covered by it (#57). */
  --banner-h: 0px;

  /* Width of the left menu (issue #26). The menu retracts to a rail of icons,
     so this is a token rather than a number: .sidebar sizes itself from it and
     main's margin clears it, which means retracting is one custom-property
     swap (see html.sidebar-collapsed) and the two can never disagree.

     --sidebar-w-rail is not a taste number. Expanded, an icon's centre sits
     14px (.sidebar padding) + 2px (.side-link's --line-sm placeholder border)
     + 12px (.side-link padding) + 12px (half a --sidebar-icon) = 40px from the
     left edge. Retracted, .side-link loses its horizontal padding and centres
     its icon, putting that centre at 14px + half the sidebar's inner width.
     Solving 14 + (w - 2*14 - --line-xl)/2 = 40 gives w = 84px, so the icons
     hold still while the rail slides in and out. Change the padding, the icon
     size or --line-xl and this has to be re-derived; test_sidebar_collapse.js
     does that arithmetic and fails if it drifts.

     --sidebar-icon is 24px because the icons are drawn on a 24-unit grid
     (#98). At any other size their strokes stop landing on whole pixels and
     blur on a 1x screen; at 20px, half of every icon's pixels were grey.

     --sidebar-brand is derived the same way the rail is (issue #166): the ask
     is a mark that is still whole once the menu is retracted, so it is exactly
     as wide as the rail can show, 84px less the 14px padding either side and
     the --line-xl right border = 52px. It is one size in both states rather
     than two, so the mark holds still while the rail slides, like the icons
     below it. Re-derive it if the rail, the padding or --line-xl move;
     test_sidebar_collapse.js does that arithmetic and fails if it drifts. */
  --sidebar-w-full: 210px;
  --sidebar-w-rail: 84px;
  --sidebar-w: var(--sidebar-w-full);
  --sidebar-icon: 24px;
  --sidebar-brand: 52px;

  /* The dashboard's board of cards (issue #218). Both numbers are tokens
     because the page's "polished" depends on them agreeing everywhere, and
     the grid is not the only place they are spent: the full-width checklist
     band sits ABOVE the grid and so has to reproduce the gap as a margin of
     its own, and any card added later (the "My downloads" card in #217/PR
     #220, for one) has to land on the same rhythm without its author having
     to go and find these values.

     --dash-gap is the single spacing unit for the whole page: the grid's row
     and column gap, the margin under the checklist band, and the margin under
     the hero. One number, so the vertical rhythm above the grid matches the
     one inside it rather than approximately matching it.

     --dash-card-min-h is the size the issue asks the cards to match: the
     height of "Progress at a glance", the card carrying a display-size
     percentage, a bar and three stat lines, and the tallest of the cards that
     share a row. It is spent as a floor rather than a height (see .dash-grid),
     so a card whose content genuinely needs more room still gets it and takes
     its row-mate up with it, instead of overflowing its own border.

     264px is that card measured in a browser at its normal state: 32px of
     padding, 6px of --line border, and 226px of title, percentage, bar, three
     stats and footer button. It is a measurement, so re-measure it if that
     card's contents change — an out-of-date value here is not a broken page,
     only a board sized to a card that no longer looks like that.
     test_dashboard.js asserts the floor is in force, not what it equals. */
  --dash-gap: 14px;
  --dash-card-min-h: 264px;
}

* { box-sizing: border-box; }

/* Keyboard focus outline (issue #265): red everywhere by default. Components
   below that give their own :focus-visible outline override the color to
   match instead of duplicating this rule. */
:focus-visible { outline: var(--line) solid var(--accent-red); outline-offset: 2px; }

/* ===== Links: white, never browser-default blue (issue #251) =====
   The lowest-specificity rule in the sheet, so every scoped link style below
   (.card, .btn, .back, .dash-headline a, ...) still wins outright. This only
   catches links that were otherwise falling through to the browser default —
   the instructor tel:/mailto: links sitting in a plain .hint paragraph on My
   Profile and the dashboard rendered dark blue on black, the one colour this
   palette does not have. */
a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: rgba(255, 255, 255, .35);
  text-underline-offset: 2px;
  transition: text-decoration-color .15s;
}
/* Issue #271: on hover a link's one red touch is the whole link, text and
   underline together, rather than just the line turning red under white
   text. */
a:hover { color: var(--accent-red); text-decoration-color: var(--accent-red); }

/* Issue #271: text selection gets the same red touch as everything else on
   the page, instead of the browser's default blue — which, like regular
   link blue, is the one colour this palette does not have. */
::selection { background: var(--accent-red); color: #ffffff; }

/* ===== Motion: reduced-motion kill switch (issue #251) =====
   Every CSS animation and transition this file adds funnels through here
   when the OS asks for less motion, instead of each rule remembering its own
   exception. The JS-driven motion — cursor tilt, the spotlight, count-up
   numbers, the page-enter stagger — checks the same media query itself (see
   motion.js) since none of that runs through a CSS transition at all. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===== Motion: page-enter + card cascade keyframes (issue #251) =====
   Shared by #app's own page-enter fade (set by motion.js on every route
   change) and by .stagger-grid's children below. Both start from opacity .35
   rather than 0 — the issue is explicit that content should never go fully
   invisible mid-transition. */
@keyframes pageIn { from { opacity: .35; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
#app.page-anim { animation: pageIn .32s ease-out; }

@keyframes cardIn { from { opacity: .35; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* Targets every card grid's real class/id directly rather than adding a
   marker class in app.js — several of these templates are matched by exact
   string in the test suite (e.g. test_dashboard.js's check that the
   onboarding checklist sits above `<div class="dash-grid">`), and a stray
   class appended there is exactly the kind of change that breaks a test like
   that for no behavioural reason. Each direct child fades/rises in ~45ms
   after the one before it; animation-fill-mode: backwards holds the *from*
   state (opacity .35) during the delay, which is what keeps a card from
   flashing at full opacity before its turn arrives. Capped at 12 explicit
   steps and a shared ceiling after that — a video library of 60 results
   cascading one every 45ms would take over 2.5s to finish, which reads as
   slow rather than lively. */
.cat-grid > *,
.dash-grid > *,
.video-grid > *,
.group-grid > *,
#docResults > * {
  animation: cardIn .32s ease-out backwards;
}
.cat-grid > *:nth-child(1), .dash-grid > *:nth-child(1), .video-grid > *:nth-child(1), .group-grid > *:nth-child(1), #docResults > *:nth-child(1) { animation-delay: 0ms; }
.cat-grid > *:nth-child(2), .dash-grid > *:nth-child(2), .video-grid > *:nth-child(2), .group-grid > *:nth-child(2), #docResults > *:nth-child(2) { animation-delay: 45ms; }
.cat-grid > *:nth-child(3), .dash-grid > *:nth-child(3), .video-grid > *:nth-child(3), .group-grid > *:nth-child(3), #docResults > *:nth-child(3) { animation-delay: 90ms; }
.cat-grid > *:nth-child(4), .dash-grid > *:nth-child(4), .video-grid > *:nth-child(4), .group-grid > *:nth-child(4), #docResults > *:nth-child(4) { animation-delay: 135ms; }
.cat-grid > *:nth-child(5), .dash-grid > *:nth-child(5), .video-grid > *:nth-child(5), .group-grid > *:nth-child(5), #docResults > *:nth-child(5) { animation-delay: 180ms; }
.cat-grid > *:nth-child(6), .dash-grid > *:nth-child(6), .video-grid > *:nth-child(6), .group-grid > *:nth-child(6), #docResults > *:nth-child(6) { animation-delay: 225ms; }
.cat-grid > *:nth-child(7), .dash-grid > *:nth-child(7), .video-grid > *:nth-child(7), .group-grid > *:nth-child(7), #docResults > *:nth-child(7) { animation-delay: 270ms; }
.cat-grid > *:nth-child(8), .dash-grid > *:nth-child(8), .video-grid > *:nth-child(8), .group-grid > *:nth-child(8), #docResults > *:nth-child(8) { animation-delay: 315ms; }
.cat-grid > *:nth-child(9), .dash-grid > *:nth-child(9), .video-grid > *:nth-child(9), .group-grid > *:nth-child(9), #docResults > *:nth-child(9) { animation-delay: 360ms; }
.cat-grid > *:nth-child(10), .dash-grid > *:nth-child(10), .video-grid > *:nth-child(10), .group-grid > *:nth-child(10), #docResults > *:nth-child(10) { animation-delay: 405ms; }
.cat-grid > *:nth-child(11), .dash-grid > *:nth-child(11), .video-grid > *:nth-child(11), .group-grid > *:nth-child(11), #docResults > *:nth-child(11) { animation-delay: 450ms; }
.cat-grid > *:nth-child(n+12), .dash-grid > *:nth-child(n+12), .video-grid > *:nth-child(n+12), .group-grid > *:nth-child(n+12), #docResults > *:nth-child(n+12) { animation-delay: 495ms; }

html {
  min-height: 100%;
}

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  /* Sticky footer (#241): body is a full-viewport-tall column so .layout can
     grow to fill the gap on short pages, pushing .site-footer to the bottom
     of the viewport instead of leaving it floating under a short main. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Headings (issue #255): every h1/h2/h3 on the site — page titles, modal
   titles, section titles, card titles — in Oswald, uppercase, with just
   enough letter-spacing to keep the caps from reading as cramped. There was
   no top-level heading rule before this (every size/weight was set per
   class), so this is the one place that now has to change for the whole
   site's headings to move together. */
h1, h2, h3 {
  font-family: var(--font-heading);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  font-weight: 600;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 20px;
  background: var(--panel);
  /* Issue #284: the header has no dividing line of its own any more — it
     floats on the page. The sidebar keeps the single thin line that used to
     also run under this bar. */
  /* Starts where the menu ends, so the bar spans exactly the same column main
     does (#139). The logo is centred in this box; main centres itself in the
     space right of the same fixed menu, so both land on one axis. While the
     bar ran the full width, its centre sat --sidebar-w/2 left of every
     centred thing on the page. Sized from the token so retracting the menu to
     the rail moves the logo with it, and transitioned to match main's margin
     so the two slide together rather than one snapping. */
  margin-left: var(--sidebar-w);
  transition: margin-left 0.2s ease;
  /* Issue #284: the bar scrolls away with the page now, not locked to the
     viewport — `relative` only so the absolutely-positioned .sidebar-toggle
     and .nav still anchor to this box. */
  position: relative;
  z-index: 15;
  gap: 12px;
}
.sidebar-toggle {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  /* Box is 44x44 (#260) — the minimum comfortable tap target — with padding
     eating the difference so the bars themselves stay the same 20px icon
     drawn before this box grew around them. */
  padding: 12px;
  color: var(--text);
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  position: absolute;
  left: 20px;
}

.sidebar-toggle span {
  width: 100%;
  height: var(--line);
  background: var(--text);
  display: block;
  transition: all 0.3s ease;
}

.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 8;
}

/* While the mobile drawer is open the page behind it stays visible but must
   not scroll (issue #256) — otherwise a swipe meant for the overlay/drawer
   scrolls the hidden page underneath it. */
body.sidebar-locked {
  overflow: hidden;
}

.brand {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  height: 50px;
}

.brand-wordmark {
  font-family: var(--font-script);
  font-size: 28px;
  line-height: 1;
  color: var(--text);
  white-space: nowrap;
  display: block;
}

/* The top bar's nav holds only the auth slot now. The .nav-link rules that
   used to sit here styled one element, the top-right Coaches link, which
   issue #153 removed as a duplicate of the left menu's Coaches row. Nothing
   on the site carries .nav-link any more, and setActiveNav() no longer looks
   for it. Keep .nav itself: it is what positions the auth slot. */
.nav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; position: absolute; right: 20px; }

/* Page layout: left sidebar + main content area.
   .sidebar is `position: fixed`, so it takes no space in this flow — .layout
   has to stay the full viewport width (no max-width/auto-margin of its own)
   or the percentages main uses below to center itself around the real,
   viewport-relative sidebar would resolve against the wrong box (#34). */
.layout {
  display: flex;
  align-items: flex-start;
  /* Grows to fill any leftover viewport height on short pages (#241), so
     .site-footer below it lands at the bottom of the screen instead of
     floating right under a short main. */
  flex: 1 0 auto;
}

.sidebar {
  flex: 0 0 var(--sidebar-w);
  width: var(--sidebar-w);
  /* The menu is its own full-height column, from y=0 to the bottom of the
     viewport (#139) — it no longer starts below the bar. That is what lets
     .topbar begin at the menu's right edge, which is what puts the logo on
     main's centre line. Its right border now runs the whole height and the
     bar's bottom border starts where that border ends.

     The rows inside it, though, have not moved a pixel: the offset is the old
     24px plus everything the box used to be pushed down by, so the first link
     still sits level with the top of the main content and still drops to clear
     the view-as banner (#57). Only the panel grew upward.

     Since #166 that offset is .side-brand's height rather than this box's top
     padding, because the band it reserves now holds the R mark instead of
     nothing. The rows have still not moved: the first one starts where the
     brand box ends, which is the same calc it always was. */
  padding: 0 14px 24px;
  /* Issue #284: the one divider between the sidebar and the page, a single
     thin 1px dark grey line — hardcoded rather than read off --border/
     --line-xl, which are tuned for the bolder lines used elsewhere. */
  border-right: 1px solid #2a2a2d;
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  height: 100vh;
  /* A column so the retract control can sit at the bottom and stay put while
     the links scroll. The clip is on this box and the scroll on .side-nav for
     the same reason: `overflow-y: auto` here would scroll the control away on
     a short viewport. Horizontally it clips the labels as the rail narrows,
     which is what makes the retract read as a slide rather than a reflow. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 10;
  background: var(--bg);
  transition: width 0.2s ease, flex-basis 0.2s ease;
}
/* The R mark above the rows (issue #166). This box is the band #139 opened up
   when the panel grew to y=0 and the rows stayed put: its height is the offset
   that used to be .sidebar's top padding, so the mark fills that space without
   pushing a single row down. flex: 0 0 auto because .side-nav below it is the
   box that scrolls — left shrinkable, a short viewport would squash the mark
   instead of scrolling the menu. */
.side-brand {
  flex: 0 0 auto;
  height: calc(24px + var(--topbar-h) + var(--banner-h));
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Square, so one token sizes both axes and the mark is whole in the rail (see
   --sidebar-brand). The PNG is a white R on an opaque black tile — the same
   file the page footer and the community groups use — and `screen` is what
   drops that tile onto the panel, as it does in .site-footer-logo. */
.side-brand-logo {
  width: var(--sidebar-brand);
  height: var(--sidebar-brand);
  display: block;
  mix-blend-mode: screen;
}

.side-nav { display: flex; flex-direction: column; gap: 4px; flex: 1 1 auto; overflow-y: auto; }
.side-link {
  color: var(--text); text-decoration: none; font-size: 15px;
  padding: 10px 12px; border-radius: 8px; border: var(--line-sm) solid transparent;
  display: flex;
  align-items: center;
  gap: 12px;
  /* The label must not re-wrap onto two lines while the rail is mid-slide; it
     is clipped by .sidebar's overflow instead. */
  white-space: nowrap;
  position: relative;
  transition: color 0.15s, background-color 0.15s;
}
.side-link:hover { color: var(--text); background: var(--panel-2); }
/* Issue #271: the icon, not the label, carries the red touch on hover — the
   label stays white so the row still reads as "selected", not "an error". */
.side-link:hover .side-icon { transform: translateX(2px) scale(1.08); color: var(--accent-red); }
.side-link.active { color: var(--text); background: var(--panel-2); border-color: var(--border); }
/* Issue #271: same touch, held rather than momentary, on the active row's
   icon — the icons are drawn with stroke="currentColor" (see ICON_SVG in
   app.js), so setting color here is enough to recolor the svg itself. */
.side-link.active .side-icon { color: var(--accent-red); }
/* The active-page marker (issue #251): a red bar (issue #265) on the left
   edge that grows in from the centre rather than appearing all at once, so
   switching pages reads as a small motion instead of a flicker. It's an
   ::before rather than a border so it can animate on its own — .active
   already owns a real border for the panel/outline look above. The animation
   re-fires naturally whenever setActiveNav() moves .active off one link and
   onto another, since that's the class going from absent to present on the
   link gaining it. */
.side-link.active::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; border-radius: 0 3px 3px 0;
  background: var(--accent-red);
  animation: sideActiveGrow .22s ease-out;
}
@keyframes sideActiveGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
/* Below 900px the menu is the off-canvas drawer (#26), where a left edge bar
   reads oddly against a full-width row — a bottom underline matches the
   drawer's own horizontal rows instead. */
@media (max-width: 900px) {
  .side-link.active::before {
    left: 10%; right: 10%; top: auto; bottom: 0;
    width: auto; height: 2px; border-radius: 2px;
    animation: sideActiveGrowX .22s ease-out;
  }
}
@keyframes sideActiveGrowX { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/* Account (issue #290): the button that replaced the standalone Log Out row.
   margin-top: auto pushes it to the bottom of the menu, the same job this
   wrap's predecessor (.side-logout) did; position: relative anchors the
   dropdown below to this box rather than the whole sidebar. */
.side-account-wrap {
  position: relative;
  margin-top: auto;
}
/* A <button> in a row of links, so it drops the button defaults. Only the
   font family is inherited, so .side-link keeps its 15px size. */
.side-account-btn {
  font-family: inherit;
  line-height: inherit;
  width: 100%;
  text-align: left;
  background: none;
  cursor: pointer;
}
/* The dropdown itself. It opens upward (bottom: 100%, not top) because the
   button it hangs off sits at the foot of the menu — opening down would run
   it off the bottom of the screen. Same panel colour, thin grey border and
   rounded corners as the site's cards, not .side-link's look: this is a
   popup, not another menu row, so it keeps its own appearance whether the
   rail is retracted or not. */
.acct-menu {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + 6px);
  background: var(--panel-2);
  border: var(--line) solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  z-index: 20;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
}
.acct-menu.open { display: block; }
.acct-menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 12px;
  background: none;
  border: none;
  color: var(--text);
  text-decoration: none;
  font-family: inherit;
  font-size: 15px;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
}
.acct-menu-item:hover,
.acct-menu-item:focus { background: #1e1e1e; color: var(--text); outline: none; }
.acct-menu-item:focus-visible { outline: var(--line) solid var(--border); outline-offset: -2px; }
/* Sized like .side-icon, but its own class: these icons sit inside a popup,
   not a menu row, so test_sidebar_collapse.js's "one icon per row" count
   should not see them. */
.acct-menu-icon {
  width: var(--sidebar-icon);
  height: var(--sidebar-icon);
  flex: 0 0 var(--sidebar-icon);
  display: block;
}
/* Log Out is a one-way action and My Profile / Profile Settings are not, so
   it is set off from them by a divider rather than just listed third. */
.acct-menu-item-danger { border-top: var(--line-sm) solid var(--border); }

/* The unread count on the Community row (issue #222).
   Inverted — white fill, black text — because this palette is black, white and
   grey only (see :root), and on a black menu of grey labels the one thing that
   has to be noticed without being looked for is the one thing drawn as a solid
   white shape. No border: the fill is already the separation, and a --line-sm
   outline around a pill this small only thickens it.
   `margin-left: auto` puts it at the far end of the row rather than trailing
   the word, so it lands in the same column whatever the label's length.
   min-width plus centring keep "3" and "99+" the same shape of pill, and
   flex: 0 0 auto stops it being squeezed as the rail slides. */
.side-badge {
  margin-left: auto;
  flex: 0 0 auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  /* Issue #329: red is the one accent this black-and-white UI allows itself,
     and an unread count is exactly the kind of thing it should be spent on.
     Kept on the shared token so #327's unread badges in the Community list
     match without a second place to update. */
  background: var(--accent-red);
  color: #ffffff;
  font-size: 12px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

/* A fixed, non-shrinking box per icon. Left flexible it would be squeezed by
   the label it shares the row with, and the icons would stop lining up. */
.side-icon {
  width: var(--sidebar-icon);
  height: var(--sidebar-icon);
  flex: 0 0 var(--sidebar-icon);
  display: block;
  transition: transform .18s ease-out;
}

.side-foot {
  flex: 0 0 auto;
  margin-top: 12px;
  padding-top: 12px;
  /* Issue #277: this is the one divider called out to be red; every other
     border still reads off --border, so the color is hardcoded here only. */
  border-top: var(--line-sm) solid #FF0000;
}
/* Matches .side-link's box exactly so the control reads as the last row of the
   menu and lands on the same icon centre when the rail is narrow. Since #29 the
   chevron is the only child, so `gap` has nothing to space and the row is all
   left padding — both are kept anyway, because it is sharing the row's box that
   puts the chevron on the icons' centre line and keeps it there, unmoved, when
   the rail takes the padding away. */
.side-collapse {
  font: inherit;
  width: 100%;
  color: var(--muted); background: none; cursor: pointer;
  padding: 10px 12px; border-radius: 8px; border: var(--line-sm) solid transparent;
  display: flex;
  align-items: center;
  gap: 12px;
  white-space: nowrap;
  transition: color 0.15s, background-color 0.15s;
}
.side-collapse:hover { color: var(--text); background: var(--panel-2); }
/* The chevron points at the edge the menu is about to move toward, so it
   turns around once the menu is already retracted. */
.collapse-chevron { transition: transform 0.2s ease; }

main {
  /* A token so a page can widen main without also re-deriving the centering
     below: the module reader does (#50). */
  --main-max: 860px;
  flex: 1 1 auto;
  min-width: 0;
  max-width: var(--main-max);
  padding: 24px 24px 64px;
  /* Centers main in the space to the right of the fixed sidebar, not in the
     viewport as a whole — the standard `margin: 0 auto` trick with the
     sidebar's width folded into the left side of the split (#34). Clamped to
     never start left of the sidebar at narrower viewport widths. */
  margin-left: max(var(--sidebar-w), calc((100% + var(--sidebar-w) - var(--main-max)) / 2));
  margin-right: auto;
  transition: margin-left 0.2s ease;
}

/* ---- the retracted menu (issue #26) ----------------------------------------
   Scoped to the wide layout on purpose. Below 900px the menu is the off-canvas
   drawer the hamburger opens, where a rail of icons means nothing — and the
   class on <html> is the user's stored preference, not the current layout, so
   it stays put while these rules simply stop applying. Widening the window
   brings the rail straight back with no JS involved. */
@media (min-width: 901px) {
  html.sidebar-collapsed { --sidebar-w: var(--sidebar-w-rail); }

  /* Out of the flow rather than transparent: a hidden-but-present label would
     still claim the rail's width and push the icon off centre. */
  html.sidebar-collapsed .side-label { display: none; }

  html.sidebar-collapsed .side-link,
  html.sidebar-collapsed .side-collapse {
    justify-content: center;
    gap: 0;
    padding-left: 0;
    padding-right: 0;
  }

  html.sidebar-collapsed .collapse-chevron { transform: rotate(180deg); }

  /* Retracted there is nothing beside the icon to put a pill next to, and a
     badge left in the flex row would widen this row alone and push its icon
     off the centre line every other row sits on. It comes out of the flow and
     onto the icon's corner instead (issue #222) — the same mark an app icon
     carries, smaller because it is now over a 24px glyph rather than beside a
     label. The count is still the link's only moving part, so the row does
     not move when it changes. */
  html.sidebar-collapsed .side-link { position: relative; }
  html.sidebar-collapsed .side-badge {
    position: absolute;
    top: 3px;
    right: 6px;
    margin-left: 0;
    min-width: 16px;
    padding: 0 4px;
    font-size: 10px;
    line-height: 15px;
    /* Issue #329: sitting on top of the icon's corner rather than beside a
       label, the red pill needs its own separation from the white glyph —
       a ring the rail's own background color, same idea as a notification
       dot cut out of the icon behind it. */
    box-shadow: 0 0 0 2px var(--panel);
  }
}

@media (max-width: 900px) {
  /* Below this width the menu is an off-canvas drawer, not a column beside the
     page, so #139's full-height menu and the inset it buys the bar both stop
     applying. The bar spans the full width again, and the drawer goes back to
     opening below it — sliding a panel over the locked bar would cover the
     hamburger that closes it. That puts the banner and the drawer back on the
     same stacking problem #57 solved, so the drawer clears both again here.

     The footer needs the same reset (#167), but it cannot join this list: its
     own rule is ~1200 lines below this block and carries equal specificity, so
     the later declaration would simply win and the reset would silently do
     nothing. It ships as its own 900px block next to .site-footer instead. */
  .topbar,
  .view-as-banner {
    margin-left: 0;
  }

  .sidebar {
    position: fixed;
    /* Sized to its content rather than the full screen (issue #256): wide
       enough that the longest label (icon + text, e.g. "Document Library")
       reads on one line with room to breathe, capped at 80% of the viewport
       so it never reads as a full-screen page. Written against the token so
       it cannot drift from the width it has to cover (issue #26); the rail
       never applies here, so this is always the full width. */
    left: calc(-1 * var(--sidebar-w));
    max-width: 80vw;
    top: calc(var(--topbar-h) + var(--banner-h));
    height: calc(100vh - var(--topbar-h) - var(--banner-h));
    padding-top: 24px;
    /* Quick ease-out (issue #251's motion convention) rather than the old
       linear-feeling `ease`. */
    transition: left 0.22s ease-out;
    z-index: 12;
  }

  .sidebar.open {
    left: 0;
  }

  /* The drawer is opened and closed by the hamburger; retracting it to a rail
     inside itself would be a second, conflicting control. */
  .side-foot {
    display: none;
  }

  /* The mark answers a gap that only the full-height panel has (#166). Here the
     drawer already starts below the bar, so there is no empty band above the
     rows, and the bar it would sit under is on screen with the wordmark in it —
     the R would be a second logo stacked a few pixels below the first, pushing
     the rows down for nothing. */
  .side-brand {
    display: none;
  }

  .sidebar-overlay.open {
    display: block;
  }
  
  .sidebar-toggle {
    display: flex;
  }
  
  /* The bars are 3px tall in a 20px content box with 5px gaps, so the outer two
     sit exactly 8px either side of the middle one. Translating in the unrotated
     frame (translate first in source order = applied last) states that 8px
     directly, instead of the old rotated-frame offsets that only happened to
     line up at a 2px bar height. */
  .sidebar-toggle.active span:nth-child(1) {
    transform: translateY(8px) rotate(45deg);
  }

  .sidebar-toggle.active span:nth-child(2) {
    opacity: 0;
  }

  .sidebar-toggle.active span:nth-child(3) {
    transform: translateY(-8px) rotate(-45deg);
  }
  
  main {
    /* The sidebar is an off-canvas overlay here, not real layout space, so
       center main in the full width instead of splitting around it (#34). */
    margin-left: auto;
    margin-right: auto;
  }
}

@media (max-width: 720px) {
  /* .brand drops 50px -> 40px below, so the bar does too. */
  :root { --topbar-h: 60px; }

  .topbar {
    padding: 10px 16px;
    gap: 8px;
  }
  
  .sidebar-toggle {
    left: 16px;
  }
  
  /* flex:1 used to stretch this link across the whole bar (#260): with
     .sidebar-toggle absolute and .nav hidden below this breakpoint, .brand
     was the only element left in the flex flow, so growing it to fill the
     bar made almost the entire header one giant tap target that happened to
     sit behind the hamburger. Dropping flex:1 shrinks the link back to its
     content — the wordmark text — while .topbar's justify-content: center
     still keeps it centered. */
  .brand {
    height: 40px;
  }

  /* Scaled down (not wrapped) so "Rogue Coach Academy" still fits on one
     line between the hamburger and the right edge at phone widths (#317). */
  .brand-wordmark {
    font-size: 19px;
  }
  
  .nav {
    display: none;
  }

  .side-nav { flex-direction: column; gap: 4px; }
  
  main {
    max-width: none;
    padding: 16px 16px 64px;
  }
}

.btn {
  font: inherit;
  /* Buttons (issue #255): Source Sans 3, bold, uppercase, with enough
     letter-spacing to keep the caps from reading as cramped. font: inherit
     above resets weight too, so these come after it to win the cascade. */
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 9px 16px;
  border-radius: 8px;
  border: var(--line) solid var(--border);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
  position: relative;
  overflow: hidden;
  transition: border-color .18s ease-out, box-shadow .18s ease-out, transform .1s ease-out;
}
/* Issue #271: an outline button's hover touch is a red border and a soft
   red glow, in place of the white ones every .btn used to hover to. */
.btn:hover {
  border-color: var(--accent-red);
  box-shadow: 0 0 0 4px rgba(255, 0, 0, .12), 0 0 16px rgba(255, 0, 0, .35);
}
.btn:active { transform: scale(.97); }
.btn:focus-visible { outline: var(--line) solid var(--accent-red); outline-offset: 2px; }
.btn:disabled { opacity: .5; cursor: default; }
/* The shine sweep (issue #251): a soft diagonal highlight that slides
   left-to-right on hover. A ::before rather than extra markup, so every .btn
   gets it for free with no change at any of its call sites. */
.btn::before {
  content: "";
  position: absolute; top: 0; left: -60%;
  width: 40%; height: 100%;
  background: linear-gradient(115deg, transparent, rgba(255, 255, 255, .5), transparent);
  transform: skewX(-20deg);
  transition: left .5s ease-out;
  pointer-events: none;
}
.btn:hover::before { left: 130%; }
.btn-primary {
  background: #000000;
  color: var(--text);
  border: var(--line) solid var(--border-strong);
  font-weight: 600;
}
/* Issue #271: a solid button's hover touch fills it red with white text,
   in place of the white-fill/black-text it used to hover to, and overrides
   .btn:hover's border/glow above to match. */
.btn-primary:hover {
  background: var(--accent-red);
  color: #ffffff;
  border-color: var(--accent-red);
  box-shadow: 0 0 0 4px rgba(255, 0, 0, .15), 0 0 18px rgba(255, 0, 0, .45);
}
.btn-small { padding: 6px 12px; font-size: 14px; }

/* .hero is the page-furniture convention the whole app is centred on (#97,
   #138): a page's own title, lede, section headings, back link and action rows
   sit on the content column's centre line, while the contents of rows, cards
   and tables stay left-aligned so they're still readable. Every "centre this
   page too" rule below points back here. */
.hero { text-align: center; margin: 30px 0 24px; }
.hero h1 { font-size: 30px; margin: 0 0 8px; }
.hero p { color: var(--muted); margin: 0; }

/* A short red rule under every page title (issue #271) — centred under the
   .hero and .detail-head h1's that cover every page in the app, so the
   title itself stays the one un-centred colour the palette uses. */
.hero h1::after,
.detail-head h1::after {
  content: "";
  display: block;
  width: 38px;
  height: 3px;
  margin: 10px auto 0;
  background: var(--accent-red);
  border-radius: 2px;
}

/* The super-admin pencil that edits a page's description (issue #154). It sits
   inline at the end of the sentence, so it has to read as punctuation rather
   than as a control: no box, no background, muted until hovered. Nobody below
   super_admin ever sees it.
   .hero-desc-empty is the same button standing alone on a page whose
   description has been cleared — without it there would be nothing left to
   click, and a cleared description could never be written again. */
.desc-edit {
  background: none; border: 0; padding: 0 2px; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 0.9em; opacity: 0.6;
  vertical-align: baseline;
}
.desc-edit:hover, .desc-edit:focus-visible { opacity: 1; color: var(--accent); }
.hero-desc-empty { font-size: 14px; }

/* Navigation is the one exception to the centring above (#197). #138 swept the
   back link in with the rest of a page's furniture and centred it; the ask was
   a misstatement, and a back link belongs in the top-left corner where it is
   the first thing read and always in the same place from page to page. A page's
   title, lede, headings and action rows stay centred around it.
   Declared rather than left to inherit: main's own text-align is the default,
   but these paragraphs sit under the centring rules above and a future centred
   ancestor would quietly take the link back with it.
   The module reader's .module-topline carries the phone drawer toggle beside
   its link and becomes a flex row below 760px, where text-align has nothing
   left to act on and the existing space-between still holds. */
p:has(> .back) { text-align: left; }

/* The one search bar style (issue #95): Coaches, Document Library and Video
   Library all render it through searchBar() in app.js. It sits at the top of
   the list, but at about half its old 56px so it no longer dominates the page:
   18px line + 3px padding twice + two --line borders = 30px. */
.search-wrap { display: flex; gap: 8px; margin: 16px 0 8px; }
.search-input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 14px;
  line-height: 18px;
  padding: 3px 12px;
  border-radius: 8px;
  border: var(--line) solid var(--border);
  background: var(--panel-2);
  color: var(--text);
  -webkit-appearance: none;
  appearance: none;
}
.search-input:focus { outline: none; border-color: var(--border-strong); }
/* iOS Safari zooms into inputs under 16px; line-height/padding keep the bar the same height */
@media (max-width: 640px) {
  .search-input { font-size: 16px; }
}

.hint { color: var(--muted); font-size: 14px; }

/* Category filter chips */
.filters { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin: 12px 0 4px; }
.chip {
  font: inherit; font-size: 14px; cursor: pointer;
  padding: 6px 13px; border-radius: 999px;
  border: var(--line-sm) solid var(--border); background: var(--panel-2); color: var(--muted);
}
.chip:hover { border-color: var(--border-strong); color: var(--text); }
.chip.active { background: #ffffff; color: #000000; border-color: #ffffff; font-weight: 600; }

.card {
  display: block;
  text-decoration: none;
  color: inherit;
  background: var(--panel);
  border: var(--line) solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  margin-bottom: 12px;
  transition: border-color .15s;
}
.card:hover { border-color: var(--border-strong); }
.card-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.card h3 { margin: 0; font-size: 18px; }
.card .team { color: var(--muted); font-size: 14px; }
.card .meta { color: var(--muted); font-size: 14px; white-space: nowrap; }
.card .summary { color: var(--muted); font-size: 14px; margin: 8px 0 0; }

/* ===== Interactive card motion: shared plumbing (issue #251) =====
   One system for every clickable card family below — skill/subsection tiles,
   dashboard tiles, document cards, video cards, community group cards — so
   the lift, border brighten, glow and cursor-tracked tilt/spotlight read as
   one language instead of five separate hovers. --tilt-x/--tilt-y and
   --spot-x/--spot-y are set from the pointer position by motion.js; a card
   that never gets a pointermove (a touch tap, keyboard focus, a browser with
   JS still loading) just keeps the defaults below — 0deg of tilt, a spotlight
   centred on the card — so this plumbing does nothing wrong without the JS. */
.cat-tile,
.dash-card,
.doc-card,
.video-card,
.group-card {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  --spot-x: 50%;
  --spot-y: 50%;
  position: relative;
}
.cat-tile::after,
.dash-card::after,
.doc-card::after,
.video-card::after,
.group-card::after {
  content: "";
  position: absolute; inset: 0; border-radius: inherit;
  background: radial-gradient(circle at var(--spot-x) var(--spot-y), rgba(255, 255, 255, .14), transparent 60%);
  opacity: 0;
  transition: opacity .25s ease-out;
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .cat-tile:hover::after,
  .dash-card:hover::after,
  .doc-card:hover::after,
  .video-card:hover::after,
  .group-card:hover::after { opacity: 1; }
}
/* Touch: skip the tilt and spotlight outright — a coarse pointer can't track
   a cursor that was never there — and just press in on tap so contact still
   reads as confirmed. motion.js adds this on pointerdown and removes it on
   pointerup/cancel. Combined with the card's own class for specificity, so it
   wins over that card family's hover transform below regardless of which
   rule the cascade would otherwise prefer. */
.cat-tile.is-touch-active,
.dash-card.is-touch-active,
.doc-card.is-touch-active,
.video-card.is-touch-active,
.group-card.is-touch-active {
  transform: scale(.985);
  transition: transform .1s ease-out;
}

/* The quiz reader carries no styling of its own — these two rules only put its
   questions and its result panel on the page's centre line (#138), under the
   .detail-head the title now sits in. */
.quiz-question,
.quiz-result { text-align: center; }

.section-title {
  font-size: 14px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--muted); margin: 26px 0 12px; border-bottom: var(--line-xl) solid var(--border);
  padding-bottom: 6px;
  /* Page furniture, so it sits on the same centre line as .hero (#138). The
     rule under it already spans the column, so only the label moves. */
  text-align: center;
}

.badge {
  display: inline-block; font-size: 11px; font-weight: 700; vertical-align: middle;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 8px; border-radius: 999px; margin-left: 6px;
}
/* Issue #271: a faint red outline on every category chip — the same small
   accent on every page that shows one, since catBadge() in app.js puts this
   on modules, quizzes, docs and videos alike. Faint enough to read as a
   touch rather than a warning. */
.badge-cat { background: var(--panel-2); color: var(--muted); border: var(--line-sm) solid rgba(255, 0, 0, .55); }
.badge-doctype { background: var(--panel-2); color: var(--text); border: var(--line-sm) solid var(--border-strong); font-weight: 600; }
.badge-sample { background: transparent; color: var(--text); border: var(--line-sm) solid var(--border-strong); }
.badge-test { background: transparent; color: var(--text); border: var(--line-sm) dashed var(--border-strong); }
/* Quizzes now sit between modules in one list (issue #15) instead of in their
   own section, so each quiz row has to identify itself. */
.badge-quiz { background: var(--panel-2); color: var(--text); border: var(--line-sm) solid var(--border-strong); }
/* Module titles are stored as "Tag | Name" (#241); the tag before the "|"
   rides along as a small pill rather than crowding the title itself. */
.badge-module-tag { background: transparent; color: var(--muted); border: var(--line-sm) solid var(--border); }
/* Quiz-passed pill (issue #265): the one other red accent besides the
   active-nav bar, the progress fill and the completed checkbox. Outlined
   rather than filled, with a small dot, so it reads as a status flag rather
   than a button. */
.badge-quiz-passed {
  background: transparent; color: var(--accent-red);
  border: var(--line-sm) solid var(--accent-red);
}
.badge-quiz-passed::before {
  content: ""; display: inline-block; width: 6px; height: 6px;
  margin-right: 5px; border-radius: 50%; background: var(--accent-red);
}
/* Instructors-only lock chip (issue #296): instructors and admins see this
   on content a coach can never see at all, so it reads as a quiet marker
   rather than a warning — the same outlined treatment as badge-sample. */
.badge-locked { background: transparent; color: var(--muted); border: var(--line-sm) solid var(--border-strong); }

.inline-link { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
.inline-link:hover { color: var(--muted); }

/* A page's action row is page furniture, so it centres like .hero (#138). This
   is what .continue-row already did on its own for the Training Hub's one
   button; every other action row now matches it instead of hanging off the
   left edge under a centred title. */
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 4px; }
.row { display: flex; gap: 12px; }
.row > label { flex: 1; }

/* The profile-style page header (#138): a title, an optional bio and the
   contact lines, all on the page's centre line the way .hero's title is on
   every list page. A .detail-body inside a .card is card content rather than a
   page header, so the bio rule is scoped to main's own children and leaves it
   alone. (The case that established this was the assigned-instructor card,
   removed in #195; the scoping still matters for every other card.) */
.detail-head { margin-bottom: 4px; text-align: center; }
.detail-head h1 { margin: 0 0 4px; font-size: 28px; }
.detail-head .team { color: var(--muted); }
.detail-body { white-space: pre-wrap; margin: 16px 0; }
main > .detail-body,
.profile-contact { text-align: center; }
/* The "1 account" / "3 videos" count and the reorder hint sit directly under a
   centred search bar or title, so they centre with it. Scoped rather than
   global: .hint is also the small print inside modals and cards. The module
   reader's column is the page on its own route, so the empty state under
   Downloads and the note under My Notes are page furniture there too. */
main > .hint,
.module-docs > .hint,
.module-notes > .hint { text-align: center; }

/* Progress bar */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin: 16px 0 22px; }
.stat-card { background: var(--panel-2); border: var(--line) solid var(--border); border-radius: 8px; padding: 14px; text-align: center; }
.stat-value { font-family: var(--font-heading); font-size: 28px; font-weight: 600; letter-spacing: 0.01em; color: var(--text); margin-bottom: 4px; }
.stat-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }

.progress { margin: 12px 0 22px; }
/* Tracks grew by 2x the border they gained. box-sizing: border-box means the
   border eats the track from the inside, and at the old heights a --line-sm
   track left too little room for the fill to read as a bar. */
.progress-bar { height: 14px; background: var(--panel-2); border-radius: 999px; overflow: hidden; border: var(--line-sm) solid var(--border); }
/* 1.1s (issue #251) rather than a snappy .25s: motion.js drops a freshly
   rendered bar's width to 0 and lets it transition up to its real value, so
   the fill reads as filling in rather than appearing already full. The same
   duration carries a completion toggle's live update to its new value. */
.progress-fill { height: 100%; background: var(--accent-red); transition: width 1.1s cubic-bezier(.16, .84, .44, 1); }
/* The bar spans the column, so its caption reads as belonging to the page
   rather than to a row, and centres with the rest of the furniture (#138). */
.progress-label { font-size: 14px; color: var(--muted); margin-top: 6px; text-align: center; }

/* Module/progress rows with a checkbox */
.mod-row {
  display: flex; align-items: center; gap: 12px;
  background: var(--panel); border: var(--line) solid var(--border);
  border-radius: 10px; padding: 12px 14px; margin-bottom: 10px;
}
.mod-row .mod-title { flex: 1; }
.mod-row .mod-title a { color: var(--text); text-decoration: none; }
.mod-row .mod-title a:hover { text-decoration: underline; }
.mod-row.done { border-color: var(--border-strong); }
.checkbox {
  width: 22px; height: 22px; flex: 0 0 auto;
  border: var(--line) solid var(--border); border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; color: transparent; font-weight: 800; user-select: none;
  transition: background-color .2s ease-out, border-color .2s ease-out;
}
.checkbox.checked { background: var(--accent-red); border-color: var(--accent-red); color: #ffffff; }
/* Completing a module (issue #251): the box briefly pops while it fills, and
   the tick draws itself in rather than appearing already finished.
   toggleCompletion() in app.js re-triggers .pop on every completion by
   removing it, forcing a reflow, then re-adding it — a plain classList.add on
   a class that's already there wouldn't restart the animation. */
.checkbox.pop { animation: checkboxPop .3s ease-out; }
@keyframes checkboxPop { 0% { transform: scale(1); } 45% { transform: scale(1.22); } 100% { transform: scale(1); } }
.checkbox-check {
  width: 14px; height: 14px;
  stroke-dasharray: 26;
  stroke-dashoffset: 26;
  opacity: 0;
  transition: stroke-dashoffset .28s ease-out, opacity .12s;
}
.checkbox.checked .checkbox-check { stroke-dashoffset: 0; opacity: 1; }

.appt {
  background: var(--panel); border: var(--line) solid var(--border);
  border-radius: 10px; padding: 12px 14px; margin-bottom: 10px;
}
.appt .when { font-weight: 600; }
.appt .who { color: var(--muted); font-size: 14px; }
.appt .note { margin: 6px 0 0; white-space: pre-wrap; }

.empty { color: var(--muted); text-align: center; padding: 36px 0; }
/* A subsection with a short list isn't broken, just not full yet (#241). */
.cat-sparse-note { color: var(--muted); text-align: center; font-size: 14px; margin: 16px 0 0; }
.back { color: var(--muted); text-decoration: none; font-size: 14px; }
.back:hover { color: var(--text); text-decoration: underline; }

/* Modal */
.modal {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.7);
  display: flex; align-items: center; justify-content: center;
  padding: 20px; z-index: 20;
}
.modal.hidden { display: none; }
.modal-card {
  background: var(--panel);
  border: var(--line) solid var(--border-strong);
  border-radius: 14px;
  padding: 22px;
  width: 100%; max-width: 460px;
  max-height: 90vh; overflow: auto;
}
.modal-card h2 { margin: 0 0 14px; }
.modal-card label { display: block; margin-bottom: 12px; font-size: 14px; color: var(--muted); }
.modal-card input, .modal-card textarea, .modal-card select {
  width: 100%; font: inherit; margin-top: 5px;
  padding: 10px 12px; border-radius: 8px;
  border: var(--line) solid var(--border); background: var(--panel-2); color: var(--text);
}
.modal-card input:focus, .modal-card textarea:focus, .modal-card select:focus { outline: none; border-color: var(--border-strong); }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 6px; }

.form-error { color: var(--danger); font-size: 14px; }
.hidden { display: none; }

/* Honeypot — visually and physically removed for real users; bots still see it. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.toast {
  position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
  background: #ffffff; color: #000000; font-weight: 600;
  padding: 11px 18px; border-radius: 10px; z-index: 30;
  box-shadow: 0 6px 20px rgba(0,0,0,.6);
}
.toast.bad { background: #1a1a1a; color: #ffffff; border: var(--line) solid var(--border-strong); }

/* Auth: top-bar login state */
.auth-slot { display: inline-flex; align-items: center; gap: 8px; }
.who-label { color: var(--muted); font-size: 14px; }
.role-tag {
  display: inline-block; font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; padding: 2px 7px; border-radius: 999px; margin-left: 2px;
  background: var(--panel-2); color: var(--text); border: var(--line-sm) solid var(--border);
}
.role-tag--viewas {
  background: #2a1f00; color: #f5c542; border-color: #7a5c00;
}

/* View-as picker (#151): the account list inside the popup. It scrolls rather
   than growing, so a long roster can't push Cancel off the bottom of the card. */
.view-as-list {
  max-height: 320px; overflow-y: auto; margin-bottom: 14px;
  border: var(--line-sm) solid var(--border); border-radius: 8px;
  background: var(--panel-2);
}
.view-as-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 12px; background: none; border: 0;
  border-bottom: var(--line-sm) solid var(--border);
  color: var(--text); font-family: inherit; font-size: 14px;
  text-align: left; cursor: pointer;
}
.view-as-row:last-child { border-bottom: 0; }
.view-as-row:hover { background: #1a1a1a; }
.view-as-row:focus-visible { outline: var(--line-xl) solid var(--accent-red); outline-offset: -2px; }
.view-as-row-name { flex: 1; }
.view-as-row.is-current { background: #2a1f00; color: #f5c542; }
.view-as-row-current {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: #f5c542;
}
.view-as-empty { margin: 0; padding: 12px; }

/* View-as active banner */
.view-as-banner {
  /* Sits directly after .topbar in the body, so it pins below it rather than
     on top of it — at top: 0 the two stick to the same line and this one's
     z-index hides the bar. */
  position: sticky; top: var(--topbar-h); z-index: 100;
  /* Indented with the bar (#139). The menu now reaches y=0, and this banner
     outranks it in paint order (z-index 100 vs 10), so at full width it would
     lay a yellow stripe across the menu's top. Sharing the bar's inset keeps
     it in the same column the bar and main occupy. */
  margin-left: var(--sidebar-w);
  background: #2a1f00; color: #f5c542;
  border-bottom: var(--line-xl) solid #7a5c00;
  padding: 7px 20px; font-size: 13px;
  display: flex; align-items: center; gap: 10px;
}
.view-as-banner .btn-ghost {
  background: transparent; color: #f5c542; border-color: #7a5c00;
  font-size: 11px; padding: 2px 10px;
}
.view-as-banner .btn-ghost:hover { background: #3d2e00; }

/* Danger button (delete) */
.btn-danger { color: var(--danger); border-color: var(--border); }
.btn-danger:hover { border-color: var(--border-strong); background: #1a1a1a; }

/* Read-only completion checkboxes (viewer can't edit this coach) */
.checkbox.static { cursor: default; }
#modList.readonly .mod-row { opacity: .92; }

/* Modal: left-aligned delete action */
.modal-actions .danger-left { margin-right: auto; }

/* Practice plan segments */
.seg-list { display: flex; flex-direction: column; gap: 8px; }
.seg-row {
  display: flex; gap: 14px; align-items: flex-start;
  background: var(--panel); border: var(--line) solid var(--border);
  border-radius: 10px; padding: 12px 14px;
}
.seg-min {
  flex: 0 0 48px; text-align: center; font-weight: 700;
  color: var(--text); background: var(--panel-2);
  border: var(--line-sm) solid var(--border); border-radius: 8px; padding: 6px 0;
}
.seg-name { font-weight: 600; }
.seg-notes { color: var(--muted); font-size: 14px; margin-top: 2px; }

/* Data table (tracking sheets) */
.table-wrap { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.data-table th, .data-table td {
  text-align: left; padding: 10px 12px; border-bottom: var(--line-sm) solid var(--border);
  vertical-align: top;
}
.data-table th {
  color: var(--muted); text-transform: uppercase; letter-spacing: .05em;
  font-size: 12px; font-weight: 700;
  /* Heavier than the row dividers so the header still separates from the body
     now that every row under it is drawn at --line-sm. */
  border-bottom: var(--line) solid var(--border);
}
.data-table tbody tr:hover { background: var(--panel-2); }
.data-table td.empty { text-align: center; padding: 24px 0; }
.data-table .row-action { text-align: right; white-space: nowrap; }

/* Account action menu (··· button + dropdown) */
.acct-menu-wrap { position: relative; display: inline-block; }
.acct-menu-btn { letter-spacing: 2px; }
.acct-dropdown {
  display: none;
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  background: var(--panel-2);
  border: var(--line) solid var(--border);
  border-radius: 6px;
  z-index: 200;
  min-width: 160px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.6);
  overflow: hidden;
}
.acct-dropdown.open { display: block; }
.acct-dropdown button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 14px;
  background: none;
  border: none;
  border-bottom: var(--line-sm) solid var(--border);
  color: var(--text);
  cursor: pointer;
  font-size: 14px;
  font-family: inherit;
}
.acct-dropdown button:last-child { border-bottom: none; }
.acct-dropdown button:hover { background: #1e1e1e; }
.acct-dropdown button.menu-danger { color: var(--danger); }

/* Video embed */
.video-embed { position: relative; width: 100%; aspect-ratio: 16 / 9; margin: 8px 0 4px; border: var(--line) solid var(--border); border-radius: 12px; overflow: hidden; background: #000; }
.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* Document libraries (Practice Plans + Tracking Sheets) */
.doc-type-filters { margin-bottom: 6px; }
.doc-card { transition: border-color .22s ease-out, background-color .22s ease-out, transform .22s ease-out, box-shadow .22s ease-out; }
/* Same lift/tilt/spotlight language as .cat-tile (issue #251). */
@media (hover: hover) and (pointer: fine) {
  .doc-card:hover {
    border-color: var(--border-strong);
    background: var(--panel-2);
    transform: perspective(700px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) translateY(-7px) scale(1.045);
    box-shadow: 0 16px 34px rgba(0, 0, 0, .5), 0 0 24px rgba(255, 255, 255, .18);
  }
}
.doc-card .doc-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 0; }
.doc-card .doc-tags .badge { margin-left: 0; }
.doc-actions { margin-top: 12px; }
.doc-actions a.btn { text-decoration: none; }

/* Shimmering placeholder blocks while a page's data is loading (issue #251),
   shaped like the cards about to land rather than a blank area. One generic
   shape covers every grid/list on the site; skeletonHtml() in app.js is what
   replaced every `<p class="hint">Loading...</p>` call site with this. */
.skel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 16px; margin-top: 8px; }
.skel-list { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
.skel-block {
  height: 120px; border-radius: 12px;
  border: var(--line) solid var(--border);
  background: linear-gradient(100deg, #0a0a0a 30%, #1c1c1c 50%, #0a0a0a 70%);
  background-size: 200% 100%;
  animation: skelShimmer 1.4s ease-in-out infinite;
  /* Issue #271: a faint red top edge on every loading placeholder. */
  box-shadow: inset 0 2px 0 rgba(255, 0, 0, .4);
}
.skel-list .skel-block { height: 64px; }
@keyframes skelShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* Category checkboxes in the upload modal */
.field-label { font-size: 14px; color: var(--muted); margin: 4px 0 6px; }
.check-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; margin-bottom: 12px; }
.check-inline { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--text); margin: 0; }
.check-inline input { width: auto; margin: 0; }
.modal-card input[type="file"] { padding: 8px; background: var(--panel-2); border: var(--line) solid var(--border); border-radius: 8px; color: var(--text); }

/* Video grid + YouTube-style cards */
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.video-card {
  margin-bottom: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column;
  transition: border-color .22s ease-out, background-color .22s ease-out, transform .22s ease-out, box-shadow .22s ease-out;
}
/* Same lift/tilt/spotlight language as .cat-tile (issue #251). overflow:
   hidden above already clips the spotlight to the card's rounded corners. */
@media (hover: hover) and (pointer: fine) {
  .video-card:hover {
    border-color: var(--border-strong);
    transform: perspective(700px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) translateY(-7px) scale(1.045);
    box-shadow: 0 16px 34px rgba(0, 0, 0, .5), 0 0 24px rgba(255, 255, 255, .18);
  }
}
/* A card whose video has a module (issue #309, finishing #301) is itself
   that module's link: the heading carries an <a> stretched over the whole
   card via ::before, so the card reads as clickable without wrapping it in
   one — the buttons and player below stay real elements and just sit above
   the stretched link in z-index so they keep their own click targets. */
.video-card-linked { cursor: pointer; }
.video-card-link { color: inherit; text-decoration: none; }
.video-card-link::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
}
.video-tags, .btn-row { position: relative; z-index: 2; }
.video-thumb {
  position: relative; z-index: 2; display: block; aspect-ratio: 16 / 9; background: var(--panel-2);
  border-bottom: var(--line) solid var(--border);
}
.video-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.video-thumb iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.video-thumb-fallback {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 13px;
}
.video-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 46px; height: 46px; border-radius: 50%;
  background: rgba(0, 0, 0, .65); border: var(--line) solid #ffffff;
}
.video-play::after {
  content: ""; position: absolute; top: 50%; left: 54%; transform: translate(-50%, -50%);
  border-style: solid; border-width: 9px 0 9px 15px; border-color: transparent transparent transparent #ffffff;
}
.video-card:hover .video-play { background: #ffffff; }
.video-card:hover .video-play::after { border-left-color: #000000; }
.video-info { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 8px; }
.video-info .card-top { gap: 8px; }
.video-info .summary { margin: 0; }

/* Skill tags on a video card (issues #20, #94) — Hitting, Pitching and so on,
   clickable to filter the library down to that skill. Smaller and quieter
   than the skill chips above the grid, which are the real controls. */
.video-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-pill {
  font: inherit; font-size: 12px; cursor: pointer;
  padding: 3px 10px; border-radius: 999px;
  border: var(--line-sm) solid var(--border); background: transparent; color: var(--muted);
}
.tag-pill:hover { border-color: var(--border-strong); color: var(--text); }
/* The app's name for a video when YouTube's differs, so a coach searching for
   either one can see why this card came back. */
.video-alt-title { margin: 0; font-size: 13px; color: var(--muted); }

/* ===== Members-only gate: hide app chrome on the landing ===== */
body.logged-out .topbar,
body.logged-out .sidebar { display: none; }
body.logged-out .layout { max-width: none; display: block; }
/* main's margin exists to clear the left menu, and the menu is hidden here, so
   the landing page was being indented by the width of something that is not on
   the page — a dead band down its left edge, the top bar's rule included. That
   predates #26, but #26 is what makes it move: with the menu width now a token,
   the indent would follow whatever the visitor last left the menu set to. */
body.logged-out main { max-width: none; padding: 0; margin-left: 0; }

/* ===== Landing / marketing page ===== */
.landing { color: var(--text); }
.landing-nav {
  display: flex; align-items: center; justify-content: center;
  padding: 18px 28px; border-bottom: var(--line-xl) solid var(--border);
  /* Same rule as .topbar: the logged-out page's bar is this one, and it had
     the same duplicate `position` shadowing its sticky. .landing-login-btn
     is absolute against this box and stays anchored without it. */
  position: sticky; top: 0; background: rgba(0,0,0,.85); backdrop-filter: blur(6px); z-index: 5;
}
.landing-brand { font-size: 18px; font-weight: 700; letter-spacing: .01em; }

.landing-login-btn {
  position: absolute;
  right: 28px;
}

.landing-hero {
  display: grid; grid-template-columns: 1.1fr .9fr; gap: 48px; align-items: center;
  max-width: 1100px; margin: 0 auto; padding: 72px 28px 56px;
}
.landing-eyebrow {
  text-transform: uppercase; letter-spacing: .16em; font-size: 12px; font-weight: 700;
  color: var(--muted); margin: 0 0 16px;
}
.landing-hero-copy h1 { font-size: 46px; line-height: 1.08; margin: 0 0 18px; }
.landing-lede { font-size: 18px; color: var(--muted); margin: 0 0 26px; max-width: 36em; }
.landing-cta { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.landing-cta .btn { padding: 12px 22px; font-size: 16px; text-decoration: none; }
.landing-fine { font-size: 13px; color: var(--muted); margin: 0; }

.landing-login-card {
  background: var(--panel); border: var(--line) solid var(--border-strong);
  border-radius: 16px; padding: 28px;
}
.landing-login-card h2 { margin: 0 0 18px; font-size: 20px; }
.landing-login-card label { display: block; margin-bottom: 14px; font-size: 14px; color: var(--muted); }
.landing-login-card input {
  width: 100%; font: inherit; margin-top: 6px; padding: 12px 14px;
  border-radius: 9px; border: var(--line) solid var(--border); background: var(--panel-2); color: var(--text);
}
.landing-login-card input:focus { outline: none; border-color: var(--border-strong); }
.landing-submit { width: 100%; padding: 12px; font-size: 16px; margin-top: 4px; }

/* Hold-to-reveal password button (issue #7): shows only while held, see password-reveal.js */
.pw-field { position: relative; display: block; margin-top: 5px; }
.modal-card .pw-field input, .landing-login-card .pw-field input { margin-top: 0; padding-right: 46px; }
.pw-reveal {
  position: absolute; top: 0; bottom: 0; right: 4px; margin: auto 0;
  width: 36px; height: 32px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 0; border-radius: 6px;
  color: var(--muted); cursor: pointer;
  touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* 24px for a 24-unit drawing: one grid unit per pixel keeps the eye crisp on 1x screens (#112) */
.pw-reveal-icon { width: 24px; height: 24px; flex: none; }
.pw-reveal:hover, .pw-reveal[aria-pressed="true"] { color: var(--text); }
.pw-reveal:focus-visible { outline: var(--line-sm) solid var(--accent-red); }

.landing-features { max-width: 1100px; margin: 0 auto; padding: 8px 28px 64px; }
.landing-section-title { text-align: center; font-size: 28px; margin: 0 0 36px; }
.landing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
.landing-feature {
  background: var(--panel); border: var(--line) solid var(--border); border-radius: 14px; padding: 22px;
  transition: border-color .15s;
}
.landing-feature:hover { border-color: var(--border-strong); }
.landing-feature h3 { margin: 0 0 8px; font-size: 18px; }
.landing-feature p { margin: 0; color: var(--muted); font-size: 15px; }

.landing-band {
  text-align: center; border-top: var(--line-xl) solid var(--border); border-bottom: var(--line-xl) solid var(--border);
  padding: 56px 28px; background: var(--panel);
}
.landing-band h2 { margin: 0 0 10px; font-size: 26px; }
.landing-band p { margin: 0 0 22px; color: var(--muted); }
.landing-band .btn { padding: 12px 26px; font-size: 16px; }

.landing-footer { text-align: center; color: var(--muted); font-size: 13px; padding: 28px; }

@media (max-width: 760px) {
  .landing-hero { grid-template-columns: 1fr; gap: 28px; padding: 48px 20px 36px; }
  .landing-hero-copy h1 { font-size: 34px; }
  .landing-nav { 
    padding: 14px 18px; 
  }
  .landing-logo {
    height: 32px;
    max-width: 140px;
  }
  .landing-login-btn {
    right: 18px;
  }
}

/* Admin-only nav items (revealed once an admin is logged in) */
.admin-only { display: none; }
body.is-admin .admin-only { display: block; }
.admin-not-super { display: none; }
body.is-admin:not(.is-super-admin) .admin-not-super { display: block; }
.super-admin-only { display: none; }
body.is-super-admin .super-admin-only { display: block; }
.coach-only { display: none; }
body.is-coach .coach-only { display: block; }
/* Staff = instructor, admin, super admin (is-content-editor honours view-as). */
.staff-only { display: none; }
body.is-content-editor .staff-only { display: block; }
/* Role-gated menu rows. The reveal rules above out-specify .side-link, so
   they were handing Accounts, Platform and (until #195) My Instructor
   `display: block` — harmless while a row was one line of text, but since #26
   a row is a flex box holding an icon and a label, and block would stack them.
   Restoring flex here rather than changing the rules above keeps them usable
   on anything that isn't a menu row. The .coach-only arm has no menu row left
   to fix, and stays so the next coach-only row is not a fresh bug. */
body.is-super-admin .side-link.super-admin-only,
body.is-coach .side-link.coach-only,
body.is-content-editor .side-link.staff-only { display: flex; }

/* Landing email-capture form */
.signup-form {
  display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
  max-width: 580px; margin: 6px auto 6px;
}
.signup-form input {
  font: inherit; padding: 12px 14px; border-radius: 9px;
  border: var(--line) solid var(--border); background: var(--panel-2); color: var(--text);
  flex: 1 1 200px; min-width: 160px;
}
.signup-form input:focus { outline: none; border-color: var(--border-strong); }
.signup-form .btn { padding: 12px 22px; flex: 0 0 auto; }
.signup-thanks { color: var(--text); font-weight: 600; }
.landing-band .form-error { margin-top: 10px; }

/* Email export date-range filter */
.date-filter { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 2px 0 10px; }
.date-filter-label { color: var(--muted); font-size: 14px; }
.date-filter label { font-size: 14px; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
.date-filter input[type="date"] {
  font: inherit; padding: 7px 10px; border-radius: 8px;
  border: var(--line-sm) solid var(--border); background: var(--panel-2); color: var(--text);
}
.date-filter input[type="date"]:focus { outline: none; border-color: var(--border-strong); }
.date-filter .btn { text-decoration: none; }

/* ===== Modules: category landing tiles ===== */
.continue-row { display: flex; justify-content: center; margin: 0 0 24px; }
.btn-continue { font-size: 16px; padding: 12px 32px; border-radius: 10px; }
.cat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 16px; margin-top: 8px; }
.cat-tile {
  font: inherit; cursor: pointer; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  background: var(--panel); color: var(--text);
  /* Issue #275: back to the normal grey card border. Issue #271 had put a red
     border here by default, but that read as too much red across the hub —
     the hover effect below still turns it red. */
  border: var(--line) solid var(--border); border-radius: 16px; padding: 28px 18px;
  transition: border-color .22s ease-out, background-color .22s ease-out, transform .22s ease-out, box-shadow .22s ease-out;
}
/* Hover lift + cursor tilt (issue #251), desktop only — see .is-touch-active
   above for the touch equivalent. --tilt-x/--tilt-y come from motion.js. */
@media (hover: hover) and (pointer: fine) {
  .cat-tile:not(.disabled):hover {
    border-color: var(--accent-red);
    background: var(--panel-2);
    transform: perspective(700px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) translateY(-7px) scale(1.045);
    /* Issue #271: a red glow on hover in place of the white one. */
    box-shadow: 0 16px 34px rgba(0, 0, 0, .5), 0 0 24px rgba(255, 0, 0, .35);
  }
}
.cat-tile.disabled { opacity: .45; cursor: default; }
.cat-tile.disabled:hover { border-color: var(--border); transform: none; }
.cat-icon {
  width: 64px; height: 64px; display: inline-flex; align-items: center; justify-content: center;
  border: var(--line) solid var(--border); border-radius: 50%; color: var(--text);
  transition: transform .22s ease-out, background-color .22s ease-out, color .22s ease-out;
}
/* Issue #271: the icon circle fills red with a white icon on hover, in
   place of the white-fill/black-icon it used to hover to. */
@media (hover: hover) and (pointer: fine) {
  .cat-tile:not(.disabled):hover .cat-icon {
    transform: rotate(-14deg) scale(1.15);
    background: var(--accent-red);
    color: #ffffff;
  }
}
/* The category icons are drawn on a 36-unit grid with a 2-unit stroke, so they
   render one unit to one pixel and every straight stroke covers exactly two
   pixel rows (#98). Scaled to any other size, they blur on a 1x screen. Both
   rings show them at 36px for that reason. */
.cat-icon svg { width: 36px; height: 36px; }
.cat-name { font-size: 17px; font-weight: 700; }
.cat-count { font-size: 13px; color: var(--muted); }

/* ===== A finished skill or subsection (issue #211) =====
   Derived from the modules inside it and never set by hand — there is no
   control anywhere that marks a skill or subsection complete — so this is
   drawn as a flat label, not as something that looks clickable. */
.cat-tile.complete { border-color: var(--good); }
/* Issue #271: the "Complete" label on a finished skill/subsection is the
   one small red touch on an otherwise white-bordered, finished tile. */
.cat-complete {
  font-size: 12px; font-weight: 700; letter-spacing: .04em; color: var(--accent-red);
}
.hero-complete { margin: 6px 0 0; }

/* ===== Modules: a skill's subsections (issue #135) =====
   The same tiles as the skill grid above, one level down, with the admin
   rename/delete buttons under each one rather than inside it: .cat-tile is a
   <button>, and a button can't legally nest another. */
.sub-tile-wrap { display: flex; flex-direction: column; gap: 8px; }
.sub-tile-wrap .cat-tile { flex: 1 1 auto; width: 100%; }
.sub-tile-admin { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
/* The skill a subsection belongs to, above its name in the page heading. */
.cat-hero-parent {
  margin: 0 0 2px; font-size: 12px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}

/* ===== Modules: one skill's module list (issue #9) ===== */
/* The two skill headings — a skill's subsections, and a subsection's module
   list — used to be a flex row: a ringed skill icon beside a column of text.
   That is why they had their own centring rule (#138); .hero's text-align
   cannot move flex items.

   Issue #196 removed the icon. It repeated the one already on the tile the
   coach clicked to get here and told them nothing the title did not, and
   centring the PAIR left the title itself half an icon-and-gap (37px) right of
   the topbar mark above it and the footer mark below it — the same class of
   misalignment #157, #174 and #177 took out of the other pages. With the icon
   gone these are plain .hero blocks, so their text centres on the content
   column the way every other page's furniture does, and the phone-width
   stacking rule they needed goes with them. */
.cat-progress { margin: 0 0 18px; }
.cat-progress-bar {
  height: 12px; border-radius: 999px; background: var(--panel-2);
  border: var(--line-sm) solid var(--border); overflow: hidden;
}
.cat-progress-bar span { display: block; height: 100%; background: var(--accent-red); transition: width 1.1s cubic-bezier(.16, .84, .44, 1); }
.cat-progress-text { margin: 8px 0 0; font-size: 13px; color: var(--muted); }
.cat-list { display: flex; flex-direction: column; border: var(--line) solid var(--border); border-radius: 12px; overflow: hidden; }
.cat-item {
  display: flex; align-items: center; gap: 12px; padding: 14px 16px;
  text-decoration: none; color: var(--text); border-bottom: var(--line) solid var(--border);
}
.cat-item:last-child { border-bottom: none; }
.cat-item:hover { background: var(--panel-2); }
.cat-item-num {
  flex: 0 0 auto; width: 26px; text-align: center;
  font-size: 13px; font-weight: 700; color: var(--muted);
}
.cat-item-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cat-item-title { font-weight: 600; }
.cat-item-sum {
  font-size: 13px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cat-item-state { flex: 0 0 auto; font-size: 12px; color: var(--muted); }
.cat-item.done .cat-item-state { color: var(--good); font-weight: 700; }
/* Issue #271: a thin red strikethrough on a completed module's own title,
   distinct from the white "Completed" state label beside it. */
.cat-item.done .cat-item-title {
  text-decoration: line-through;
  text-decoration-color: var(--accent-red);
  text-decoration-thickness: 1px;
}
/* The shared .mod-check is an empty box; the list spells the tick out. */
.cat-item .mod-check.on::after { content: "✓"; }

/* Admin-editable module list (issue #12). The row is a container rather than a
   link here, so the inner .cat-item-info carries the link styling instead. */
.cat-admin-bar { align-items: center; gap: 12px; margin: 10px 0; }
.cat-admin-bar .hint { margin: 0; }
.cat-item-edit { cursor: default; }
.cat-item-edit .cat-item-info { text-decoration: none; color: var(--text); }
.cat-item-edit .cat-item-info:hover .cat-item-title { text-decoration: underline; }
.cat-item-admin { flex: 0 0 auto; display: flex; gap: 6px; }
.cat-drag {
  flex: 0 0 auto; cursor: grab; background: none; border: none;
  color: var(--muted); font-size: 16px; line-height: 1; padding: 4px 2px;
}
.cat-drag:hover { color: var(--text); }
.cat-drag:focus-visible { outline: var(--line) solid var(--accent-red); border-radius: 4px; }
.cat-item-edit.dragging { opacity: 0.45; }

/* Issue #72: every module and quiz is its own card, styled like the
   "+ Add module" / "+ New quiz" buttons, rather than a row in one outlined
   box. Issue #96: coaches get the same cards on their read-only list. */
.cat-list-cards {
  gap: 8px;
  border: none; border-radius: 0; overflow: visible;
}
.cat-list-cards .cat-item,
.cat-list-cards .cat-item:last-child {
  border: var(--line) solid var(--border); border-radius: 8px;
  background: var(--panel-2);
  transition: transform .15s ease, border-color .15s ease;
}
.cat-list-cards .cat-item:hover { border-color: var(--border-strong); }
@media (prefers-reduced-motion: reduce) {
  .cat-list-cards .cat-item { transition: none; }
}

/* While a card in the editable list is held, the two it would land between
   are pushed apart (--gap-open), outlined in white, and a marker line fills
   the space between them. */
.cat-list-edit { --gap-open: 24px; position: relative; }
.cat-list-edit.is-dragging { padding-bottom: var(--gap-open); }
.cat-list-edit.is-dragging .cat-item-edit:hover { border-color: var(--border); }
.cat-list-edit .cat-item-edit.gap-shift { transform: translateY(var(--gap-open)); }
.cat-list-edit .cat-item-edit.gap-neighbour { border-color: var(--border-strong); }
.cat-drop-line {
  display: none; position: absolute; left: 0; right: 0;
  height: var(--line-xl); margin-top: calc(var(--line-xl) / -2);
  background: var(--accent); border-radius: 999px; pointer-events: none;
}
.cat-list-edit.has-gap .cat-drop-line { display: block; }

@media (max-width: 640px) {
  .cat-item-admin { flex-wrap: wrap; justify-content: flex-end; }
}

@media (max-width: 640px) {
  .cat-item-sum { display: none; }
}

/* ===== Modules: reader (content + category list + prev/next) ===== */
/* The video is the point of a module page (#50). Inside the standard 860px
   main, with the category list beside it, the player came out ~530px wide,
   no bigger than the downloads and notes below it. The reader gets a wider
   main and a narrower list, so the player is the largest thing on the page. */
main:has(> .module-reader) {
  --main-max: 1180px;
  /* The room .module-side takes out of the right of main: its own 220px plus
     the 22px .module-reader gap it would have sat after. Still a token rather
     than a literal so the phone width can zero it in one edit. #158 also had
     the back-link row read it, to pull that row's centre line onto
     .module-main's; #197 left-aligns the link, so there is no centre to match
     any more and that rule is gone — .module-main is the only reader left. */
  --module-side-gutter: 242px;
}
.module-reader { display: flex; gap: 22px; align-items: flex-start; }
.module-main {
  flex: 1 1 auto; min-width: 0;
  /* .module-side is pinned to the viewport edge below (#36) rather than laid
     out in this flex row, so this reserves the space it used to occupy to
     keep the video from sliding under it. */
  margin-right: var(--module-side-gutter);
}
.module-side {
  /* Pinned to the true right edge of the screen (#36), same pattern as
     .sidebar on the left, rather than sticky within the centered column —
     sticky only froze the vertical position, so centering main (#42) dragged
     the list inward with it on wide viewports. `right` mirrors main's own
     right padding, so the list lines up where it already sat before main
     started centering. Bounded to the viewport height with its own scroll,
     since it's no longer in the page's flow to grow into. z-index stays below
     .sidebar-overlay (8) so the drawer's dimming backdrop still covers it, but
     above normal page content. */
  position: fixed;
  top: calc(var(--topbar-h) + var(--banner-h));
  right: 24px;
  width: 220px;
  max-height: calc(100vh - var(--topbar-h) - var(--banner-h) - 24px);
  display: flex;
  flex-direction: column;
  border: var(--line) solid var(--border); border-radius: 12px; overflow: hidden;
  background: var(--bg);
  z-index: 7;
}
.module-side-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 12px 14px; font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); border-bottom: var(--line) solid var(--border);
  background: var(--panel-2);
}
/* Scrolls within .module-side's own bounded height (#36) rather than the
   head, so a long module list doesn't push the box past the viewport. */
.mod-side-list { display: flex; flex-direction: column; flex: 1 1 auto; overflow-y: auto; }
.mod-side-item {
  display: flex; align-items: center; gap: 10px; padding: 11px 14px;
  text-decoration: none; color: var(--text); font-size: 14px;
  border-bottom: var(--line-sm) solid var(--border);
}
.mod-side-item:last-child { border-bottom: none; }
.mod-side-item:hover { background: var(--panel-2); }
/* The current-item marker is an accent stripe like .notif-item.unread, so it
   scales with them — at the old 3px it now merely matched the dividers. */
.mod-side-item.current { background: var(--panel-2); font-weight: 600; box-shadow: inset var(--line-accent) 0 0 var(--border-strong); }
.mod-side-title { flex: 1; min-width: 0; }
.mod-check {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 5px;
  border: var(--line-sm) solid var(--border); display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 800; color: transparent;
}
.mod-check.on { background: var(--accent-red); border-color: var(--accent-red); color: #fff; }

/* Phone-only drawer controls (#114). Beside the content the list needs no
   opening or closing, so wide screens never show these; the 760px block below
   turns them on. */
.module-side-toggle,
.module-side-close,
.module-side-backdrop { display: none; }
.module-side-toggle-icon {
  width: 16px; height: 16px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}
.module-side-toggle-pos { color: var(--muted); font-variant-numeric: tabular-nums; }
.module-side-close {
  align-items: center; justify-content: center;
  width: 32px; height: 32px; margin: -6px -8px -6px 0; padding: 0;
  background: none; border: none; border-radius: 8px;
  color: var(--text); font-size: 24px; line-height: 1; cursor: pointer;
}
.module-side-close:hover { background: var(--panel); }

/* Prev/Next are navigation, so #197 puts them on the left edge with the back
   link rather than pushed to the two ends by `space-between` — "← Previous",
   the count and "Next →" now read as one left-aligned group on the same axis.
   The row stays at the foot of the reader: it is the control you reach for
   after finishing the module, and above the video it would sit before the
   content it pages through. */
.module-nav {
  display: flex; align-items: center; justify-content: flex-start; gap: 12px;
  margin-top: 26px; padding-top: 16px; border-top: var(--line-xl) solid var(--border);
}
.module-nav .btn { text-decoration: none; }
.module-nav-pos { color: var(--muted); font-size: 13px; }
.nav-disabled { opacity: .4; cursor: default; }

/* Downloadable documents attached to a module */
.module-docs { margin-top: 26px; }
.module-doc {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; margin-bottom: 8px;
  background: var(--panel-2); border: var(--line) solid var(--border); border-radius: 10px;
}
.module-doc-icon { font-size: 18px; line-height: 1; }
.module-doc-info { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.module-doc-title { font-weight: 600; }
.module-doc-meta { color: var(--muted); font-size: 13px; }
.module-doc .btn { text-decoration: none; white-space: nowrap; }

/* Summary + key coaching cues (issue #242), directly under the video. Same
   smaller heading treatment as the per-coach notes below, so neither reads
   as heavier than the video itself. */
.module-learn { margin-top: 18px; }
.module-learn .section-title { font-size: 12px; margin: 0 0 6px; padding-bottom: 4px; }
.module-learn p { margin: 0 0 4px; white-space: pre-wrap; }
.module-cues { margin: 0; padding-left: 20px; }
.module-cues li { margin: 2px 0; }

/* Per-coach module notes (inside the module reader). Kept deliberately small
   (#50): a short box that grows with resize, and a smaller heading, so it
   reads as secondary to the video. */
.module-notes { margin-top: 22px; }
.module-notes .section-title { font-size: 12px; margin: 0 0 6px; padding-bottom: 4px; }
.module-notes .hint { font-size: 13px; margin: 0 0 8px; }
.module-notes .note-area { min-height: 72px; padding: 8px 12px; }
.note-area {
  width: 100%; box-sizing: border-box; resize: vertical;
  min-height: 120px; padding: 12px 14px; font: inherit; line-height: 1.5;
  background: var(--panel-2); color: var(--text);
  border: var(--line) solid var(--border); border-radius: 10px;
}
.note-area:focus { outline: none; border-color: var(--accent, #4f86f7); }
.note-actions { align-items: center; gap: 12px; margin-top: 10px; }
.note-status { color: var(--muted); font-size: 13px; }
.note-saved-at { color: var(--muted); font-size: 13px; }

/* Collected notes on the coach profile */
.coach-notes { display: flex; flex-direction: column; gap: 12px; }
.note-card {
  background: var(--panel-2); border: var(--line) solid var(--border);
  border-radius: 10px; padding: 12px 14px;
}
.note-card-head {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 8px;
}
.note-card-title {
  font-family: var(--font-heading); font-weight: 600; text-decoration: none;
  text-transform: uppercase; letter-spacing: 0.01em;
}
.note-card-title:hover { text-decoration: underline; }
.note-card-date { color: var(--muted); font-size: 12px; margin-left: auto; }
.note-card-body { white-space: pre-wrap; line-height: 1.5; }

/* Platform management page (super-admin hub) */
.platform-section {
  background: var(--panel-2); border: var(--line) solid var(--border);
  border-radius: 10px; margin: 0 0 20px; overflow: hidden;
}
.platform-section-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px; border-bottom: var(--line) solid var(--border); gap: 12px;
}
.platform-section-title { display: flex; align-items: center; gap: 10px; }
.platform-section-title h2 { margin: 0; font-size: 17px; }
.platform-section-body { padding: 16px 20px; }
.platform-section-body .section-title { margin-top: 20px; }
.platform-section-body .section-title:first-child { margin-top: 0; }
.platform-badge {
  display: inline-flex; align-items: center; font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; padding: 2px 9px; border-radius: 999px;
}
.platform-badge-on  { background: var(--panel-2); color: var(--text); border: var(--line-sm) solid var(--border-strong); }
.platform-badge-off { background: var(--panel-2); color: var(--muted); border: var(--line-sm) solid var(--border); }
.platform-ann-preview { margin: 0 0 4px; white-space: pre-wrap; line-height: 1.5; }

/* Analytics dashboard inside Platform page */
.analytics-group { margin: 0 0 22px; }
.analytics-group-label {
  font-size: 11px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 10px;
}
.analytics-top-modules { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 4px; }
.analytics-top-modules th {
  text-align: left; font-size: 11px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted);
  padding: 0 10px 8px 0; border-bottom: var(--line) solid var(--border);
}
.analytics-top-modules td { padding: 8px 10px 8px 0; border-bottom: var(--line-sm) solid var(--border); color: var(--text); }
.analytics-top-modules tr:last-child td { border-bottom: none; }
.analytics-top-modules .rank { color: var(--muted); font-size: 13px; width: 24px; }
.analytics-top-modules .count { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.analytics-stat-highlight { color: var(--text); font-weight: 700; }
.analytics-loading { color: var(--muted); font-size: 14px; padding: 8px 0; }

/* Login announcement popup + super-admin editor */
.announcement-card { max-width: 520px; }
.announcement-flag {
  display: inline-block; font-size: 12px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent, #4f86f7);
  background: color-mix(in srgb, var(--accent, #4f86f7) 14%, transparent);
  padding: 3px 10px; border-radius: 999px; margin-bottom: 12px;
}
.announcement-title { margin: 0 0 10px; font-size: 20px; }
.announcement-body { white-space: pre-wrap; line-height: 1.55; color: var(--text); }
.check-row {
  display: flex; align-items: center; gap: 10px;
  flex-direction: row; margin-top: 4px; font-weight: 500;
}
.check-row input[type="checkbox"] { width: auto; margin: 0; }

/* "Ask Coach Bill" how-to steps (issue #239) */
.ask-bill-steps { margin: 14px 0 0; padding-left: 20px; line-height: 1.5; }
.ask-bill-steps li { margin-bottom: 6px; }

/* On a phone the list used to stack above the content (order: -1), so every
   module in the category had to be scrolled past to reach the video (#114).
   It is now a drawer parked just off the right edge, and the page opens on the
   content. The "Modules" button in the back-link row slides it in;
   module-side-drawer.js puts .side-open on .module-reader. It keeps the
   z-index it has on wide screens — under .sidebar-overlay — so the hamburger's
   drawer still covers it, and the backdrop sits one step below it. Closed, it
   is visibility: hidden as well as off-screen, so its links drop out of the tab
   order; the visibility change waits for the slide to finish. */
@media (max-width: 760px) {
  .module-reader { flex-direction: column; }
  /* Nothing is pinned to the right at this width — the list is the drawer
     below — so the gutter goes away for .module-main and the back-link row
     together, and the topline's flex row gets its full width back. */
  main:has(> .module-reader) { --module-side-gutter: 0px; }
  .module-topline {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
  }
  .module-side-toggle { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; }
  .module-side-close { display: inline-flex; }
  .module-side {
    top: calc(var(--topbar-h) + var(--banner-h));
    right: 0;
    bottom: 0;
    width: min(300px, 85vw);
    max-height: none;
    border-radius: 0;
    border-top: none; border-right: none; border-bottom: none;
    transform: translateX(100%);
    visibility: hidden;
    transition: transform 0.3s ease, visibility 0s linear 0.3s;
  }
  .module-reader.side-open .module-side {
    transform: none;
    visibility: visible;
    transition: transform 0.3s ease;
  }
  .module-reader.side-open .module-side-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.5);
    z-index: 6;
  }
}
@media (max-width: 760px) and (prefers-reduced-motion: reduce) {
  .module-side,
  .module-reader.side-open .module-side { transition: none; }
}

/* ---- Profile / roster (profile-landing feature) ------------------------- */
.role-pill {
  display: inline-block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--muted);
  border: var(--line-sm) solid var(--border);
  border-radius: 999px;
  padding: 1px 9px;
  margin-left: 8px;
  vertical-align: middle;
}
/* No .roster-wrap scroller (issue #127). The roster lost its Downloads and
   Phone columns and its progress bar, so it now fits without scrolling
   sideways, and the scroller had to go with them: what it hid on a narrower
   window was its own right edge, which is the actions column holding the
   "Assign instructor" menu. The page scrolls if a row ever gets wide again;
   a column nobody can see is worse than a page that scrolls. */
table.roster {
  width: 100%;
  border-collapse: collapse;
  margin-top: 10px;
  font-size: 15px;
}
table.roster th,
table.roster td {
  text-align: left;
  padding: 10px 12px;
  border-bottom: var(--line-sm) solid var(--border);
  vertical-align: middle;
}
table.roster th {
  color: var(--muted);
  font-weight: 600;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: .03em;
  border-bottom: var(--line) solid var(--border);
}
table.roster tr:hover td { background: var(--panel-2); }
table.roster a { color: var(--text); }
/* Module progress on the roster is the percent and nothing else (issue #127).
   The .mini-progress* track that used to sit beside it is gone: it was the
   widest thing in the row and said no more than the number next to it. */
.roster-pct {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* On a phone the login email is the widest thing left in the row, and it is
   the one column an admin can look up elsewhere. Dropping it keeps the actions
   menu on screen, which is the whole point of issue #127. */
@media (max-width: 640px) {
  .roster-email { display: none; }
}

/* ===== Site-wide footer (logged-in pages — R logo) ===== */
.site-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
  /* No border-top, on purpose (#168). Everywhere else on the site a --line-xl
     rule marks a division between parts of the page; here there is nothing to
     divide — main ends and the mark sits below it, on the same black. The
     footer carries the page background rather than a panel so that the end of
     the content and the start of the footer are one uninterrupted surface.
     Re-adding a rule here puts the line straight back. */
  background: var(--bg);
  margin-top: 40px;
  /* Starts where the menu ends, so the mark centres in the same column the bar
     and main do (#167). The footer is a sibling of .layout in the body, so at
     full width its centre sat --sidebar-w/2 left of every other centred thing
     on the page — the same off-by-half-a-menu #139 fixed for the bar. Sized
     from the token so retracting the menu to the rail carries the mark with
     it, and transitioned to match .topbar and main so all three slide
     together rather than the logo snapping after them. */
  margin-left: var(--sidebar-w);
  transition: margin-left 0.2s ease;
  /* Issue #271: a short red rule centred above the footer. This is a small
     accent mark, not the full-width divider #168 deliberately left off this
     element, so it does not reopen that decision — position: relative just
     gives the ::before below a box to sit in. */
  position: relative;
}
/* Below 900px the menu is the off-canvas drawer, so it occupies no column and
   main goes back to `margin-left: auto` on the full width. The footer drops the
   inset to stay on that axis, or the mark sits a menu-width right of the page.
   This block has to live AFTER the rule above rather than with .topbar's reset
   up at the drawer breakpoint: both selectors are a single class, so on equal
   specificity source order decides, and from up there it loses. */
@media (max-width: 900px) {
  .site-footer { margin-left: 0; }
}
.site-footer::before {
  content: "";
  position: absolute;
  top: 0; left: 50%;
  width: 28px; height: 3px;
  margin-left: -14px;
  background: var(--accent-red);
  border-radius: 2px;
}
.site-footer-logo {
  height: 44px;
  width: auto;
  opacity: 0.6;
  mix-blend-mode: screen;
  transition: opacity .2s;
}
.site-footer-logo:hover { opacity: 1; }
/* Hide global footer on landing page (has its own footer with logo) */
body.logged-out .site-footer { display: none; }

/* ===== Landing: logo in nav ===== */
.landing-logo {
  height: 45px;
  width: auto;
  max-width: 220px;
  display: block;
}

/* ===== Landing: action photo strip ===== */
.landing-photo-strip {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  max-width: 600px;
  margin: 40px auto;
  padding: 0 28px;
  overflow: visible;
  width: 100%;
}
.landing-photo-strip img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 12px;
  border: var(--line) solid var(--border);
  display: block;
  filter: brightness(0.85) contrast(1.03);
  transition: filter .25s, transform .2s;
}
.landing-photo-strip img:hover { filter: brightness(1) contrast(1.03); transform: scale(1.015); }

/* ===== Landing: action photo gallery ===== */
.landing-gallery {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  max-width: 600px;
  margin: 0 auto 56px;
  padding: 0 28px;
  width: 100%;
}
.landing-gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 12px;
  border: var(--line) solid var(--border);
  display: block;
  filter: brightness(0.85) contrast(1.03);
  transition: filter .25s, transform .2s;
}
.landing-gallery img:hover { filter: brightness(1) contrast(1.03); transform: scale(1.015); }

/* ===== Landing: footer with R logo ===== */
.landing-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.landing-footer-logo {
  height: 40px;
  width: auto;
  opacity: 0.55;
  mix-blend-mode: screen;
  transition: opacity .2s;
}
.landing-footer-logo:hover { opacity: 1; }
.landing-footer-copy { color: var(--muted); font-size: 13px; margin: 0; }



/* ===== Notifications ===== */
.notif-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  margin-left: 6px;
  background: #e53e3e;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 9px;
  vertical-align: middle;
}
.notif-badge.hidden { display: none; }

.notif-list { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }

.notif-item {
  background: var(--card-bg, #fff);
  border: var(--line) solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  display: flex;
  gap: 12px;
  align-items: flex-start;
  transition: opacity .2s;
}
.notif-item.unread { border-left: var(--line-accent) solid var(--accent, #3182ce); }
.notif-item.read { opacity: 0.65; }

.notif-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; }
.notif-content { flex: 1; min-width: 0; }
.notif-title { font-weight: 600; font-size: 14px; margin: 0 0 4px; }
.notif-body  { font-size: 13px; color: var(--muted); margin: 0 0 6px; }
.notif-meta  { font-size: 12px; color: var(--muted); }

.notif-actions { display: flex; gap: 8px; margin-top: 16px; }

/* ===== Notification settings ===== */
/* Centered under the page's centered .hero header (#97). Only the column is
   centered; the rows inside stay left-aligned so the checkboxes line up. */
.notif-settings-form { max-width: 540px; margin: 0 auto; }

.notif-pref-row {
  padding: 14px 0;
  border-bottom: var(--line-sm) solid var(--border);
}
.notif-pref-row:last-of-type { border-bottom: none; }

.notif-pref-label {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
}
.notif-pref-label input[type="checkbox"] {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  accent-color: var(--accent, #3182ce);
}
.notif-pref-name { flex: 1; }
.notif-pref-desc { margin: 4px 0 0 26px; font-size: 13px; color: var(--muted); }

.notif-save-msg { font-size: 13px; color: var(--muted); margin-left: 10px; }

/* ---- Forgot / reset password ---------------------------------------------- */
.forgot-row { margin: 12px 0 0; text-align: center; }
.forgot-link { color: var(--muted); font-size: 13px; text-decoration: none; }
.forgot-link:hover { color: var(--text); text-decoration: underline; }
.form-ok { color: var(--text); font-size: 14px; margin: 6px 0 0; }

.landing-login-card .forgot-row { text-align: left; }

/* Standalone reset-password screen (reachable while logged out) */
.reset-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center; padding: 40px 16px; }
.reset-card {
  width: 100%; max-width: 380px; background: var(--panel, #111);
  border: var(--line) solid var(--border, #222); border-radius: 12px; padding: 28px 26px;
}
.reset-card h2 { margin: 0 0 16px; }
.reset-card label { display: block; margin-bottom: 12px; font-size: 14px; color: var(--muted); }
.reset-card input {
  width: 100%; box-sizing: border-box; margin-top: 6px; padding: 10px 12px;
  background: var(--panel-2, #000); border: var(--line) solid var(--border, #333);
  border-radius: 8px; color: var(--text); font-size: 15px;
}
.reset-card input:focus { outline: none; border-color: var(--border-strong, #666); }
.reset-card .btn { width: 100%; margin-top: 6px; }

/* ---- Community: chat groups (issue #142) ---------------------------------
   Two pages: a grid of group cards, and one group's conversation. Both reuse
   the site's card + btn-row furniture; what is new here is the group tile
   (picture or initials) and the message log. */
.group-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.group-card {
  display: flex; align-items: center; gap: 14px; margin-bottom: 0;
  transition: border-color .22s ease-out, background-color .22s ease-out, transform .22s ease-out, box-shadow .22s ease-out;
}
/* Same lift/tilt/spotlight language as .cat-tile (issue #251). */
@media (hover: hover) and (pointer: fine) {
  .group-card:hover {
    border-color: var(--border-strong);
    background: var(--panel-2);
    transform: perspective(700px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) translateY(-7px) scale(1.045);
    box-shadow: 0 16px 34px rgba(0, 0, 0, .5), 0 0 24px rgba(255, 255, 255, .18);
  }
}
.group-info { flex: 1; min-width: 0; }
.group-info h3 { margin: 0 0 4px; overflow-wrap: anywhere; }
.group-last { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
.group-actions { margin-top: 0; }

/* Pictures — people and community groups alike (issue #141).
   ------------------------------------------------------------------------
   One treatment for both, because the issue asks for one: a circle, no border
   line, sitting straight on the page background. So no border, no box-shadow,
   and no fill behind the image — the photo is an object on the page rather
   than something in a frame. `.group-avatar` is kept as an alias of `.avatar`
   so the Community markup (issue #142) picks the change up unchanged.

   The two fallbacks are the exceptions to "no fill", for opposite reasons.
   A community group with no picture still falls back to initials, and two
   letters floating with nothing behind them don't read as a picture at all,
   so that placeholder keeps its tinted disc. A person with no picture now
   shows the Rogue R instead (issue #229), which is a real image and needs the
   black it was drawn on rather than a tint. Both are still circles, and
   neither gains a border line. */
.avatar, .group-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; overflow: hidden; border-radius: 50%;
  background: none; border: 0;
}
.avatar img, .group-avatar img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  /* Also on the img: overflow:hidden on the parent already clips it, but a
     broken or still-loading image paints its own box, and a square flash
     before the load settles is exactly the border-ish edge this rule set is
     here to avoid. */
  border-radius: 50%;
}
.group-avatar--initials {
  font-weight: 700; letter-spacing: .04em; color: var(--text);
  background: var(--panel-2);
}
/* The R an account with no picture of its own falls back to (issue #229).
   #000 rather than a theme variable on purpose: it is the logo file's own
   field, so the disc and the image are the same black and the seam between
   them never shows — while the image loads, or if it fails to. */
.avatar--default { background: #000; }
.avatar--sm, .group-avatar--sm { width: 36px; height: 36px; font-size: 13px; }
.avatar--md, .group-avatar--md { width: 56px; height: 56px; font-size: 18px; }
.avatar--lg, .group-avatar--lg { width: 84px; height: 84px; font-size: 26px; }
.avatar--xl, .group-avatar--xl { width: 112px; height: 112px; font-size: 34px; }

/* The picture at the top of a profile, centred over the name the way
   .detail-head centres everything else on that page. */
.profile-photo { text-align: center; margin: 0 0 12px; }
.profile-photo .btn-row { justify-content: center; margin-top: 10px; }
/* The "you still need a picture" line staff see until they upload one. */
.photo-required-note { text-align: center; color: var(--danger); margin: 8px 0 0; }

/* A picture inline with text — the roster's name cell and the "Instructor:"
   line on a profile. The link keeps its own colour; the picture just sits
   beside the words. */
.roster-name > a, .instructor-line {
  display: inline-flex; align-items: center; gap: 8px;
}
.instructor-line { justify-content: center; }

/* .instructor-card and .instructor-ident styled My Instructor's card (#175)
   and went with that page in issue #195. What they did — lead the card with
   the person, and keep the contact links muted so only the underline marks
   them clickable — is now .dash-person's job on the dashboard's instructor
   card. .instructor-line just above is a different thing and stays: it is the
   "Instructor:" line on a profile card (#127), which nothing removed. */

.group-header { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.group-header-info { flex: 1; min-width: 200px; }
.group-header-info h1 { margin: 0 0 4px; }
.group-header-actions { margin-top: 0; }

/* The conversation. Scrolls on its own so the compose box stays reachable on
   a long log; app.js pins it to the newest post after a render, and again
   when a live update (#204) lands for a reader who was already at the bottom.
   One who had scrolled up into the history is left where they were. */
.chat-log {
  border: var(--line) solid var(--border); border-radius: 12px;
  padding: 14px 16px; max-height: 58vh; overflow-y: auto;
  background: var(--panel);
}
.chat-day {
  margin: 14px 0 10px; text-align: center; color: var(--muted);
  font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
}
.chat-day:first-child { margin-top: 0; }
.chat-msg { padding: 8px 0; border-top: var(--line-sm) solid var(--border); }
.chat-msg:first-child { border-top: none; }
.chat-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 3px; }
/* Posts are never anonymous, so the author's name is the loudest thing on
   the row — not the timestamp, and not the body. */
.chat-author { font-weight: 700; }
.chat-time { color: var(--muted); font-size: 12px; }
.chat-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.chat-msg--own .chat-author { text-decoration: underline; text-underline-offset: 3px; }

.chat-compose { margin-top: 14px; }
.chat-compose textarea { width: 100%; resize: vertical; }
.chat-locked { margin-top: 14px; text-align: center; }
.chat-locked .hint { margin-bottom: 10px; }

/* Membership popup: the list, then the add picker. */
.member-list { list-style: none; margin: 0 0 16px; padding: 0; }
.member-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 0; border-bottom: var(--line-sm) solid var(--border);
}
.member-row:last-child { border-bottom: none; }
.member-name { flex: 1; min-width: 120px; font-weight: 600; }
.member-note { font-size: 12px; }
.inline-check { display: flex; align-items: center; gap: 8px; margin: 4px 0 12px; }

/* ---- Dashboard (issue #181) ------------------------------------------------
   The post-login landing page: a board of cards, each one a single question
   answered ("where was I", "what's next", "how far along am I").

   The layout's one structural requirement comes from the onboarding checklist
   that issue #182 will add and that will later disappear again once a coach
   completes it. Both of those events have to cost the rest of the page
   nothing, so the checklist is a full-width band ABOVE the grid rather than a
   cell inside it, and the grid itself is auto-fit rather than a fixed column
   count — an absent card closes up instead of leaving a hole, and an arriving
   one pushes nothing sideways. */
.dash-hero { margin-bottom: var(--dash-gap); }

/* The dashboard was the one page with no page title (#226): its <h1> was the
   greeting, so a coach landing here saw "Welcome back, ..." where every other
   page names itself first. "Dashboard" takes the <h1> the way "Community" and
   "Video Library" do — heavier than .hero h1's default so it reads as the
   page header the issue asks for — and the greeting drops to the line below
   it. It is NOT a .hero lede: it keeps the body colour rather than going
   muted, because it is a greeting and not a description of the page. */
.dash-title { font-weight: 600; }
.dash-greeting { font-size: 18px; font-weight: 600; }
.hero .dash-greeting { color: var(--text); }

/* grid-auto-rows and the absence of align-items are the whole of issue #218.
   The cards used to fit their content, so a row of them read as a ragged
   skyline — a two-line "Notes" card beside a "Continue where you left off"
   card beside the tall progress card, each a different height, each with its
   button at a different level.

   Two rules, and it matters which does what:

   minmax(--dash-card-min-h, auto) is the row height, and the floor is the
   size the issue names — the progress card's. Every row is at least that, so
   the short cards are drawn at the size of "Progress at a glance" rather than
   at the size of their own sentence.

   align-items is simply absent, so cards take the grid's default stretch and
   fill the row they are in. align-items: start used to be here, and it was
   the rule opting every card OUT of that stretch; removing it is what makes
   two cards sharing a line the same height as each other even when the row
   has had to grow past the floor for one of them.

   Why the floor rather than grid-auto-rows: 1fr, which would make every row
   on the page exactly equal: 1fr equalises UPWARD to the tallest row, and the
   tallest row is the full-width community card carrying four group rows, ~85px
   taller than the progress card. Every short card would be stretched to that,
   and since .dash-card-body's flex:1 pins each footer to the bottom edge, the
   Notes card would be a sentence, 200px of nothing, and a button. The
   community card is full width and alone on its row, so there is nothing
   beside it to be ragged against and no reason for its list to set the size of
   the whole page.

   This is deliberately a property of the GRID and of .dash-card, not of any
   named slot, so a card added to this page inherits the sizing by being a
   .dash-card — nothing to remember, nothing to opt in to. */
.dash-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  grid-auto-rows: minmax(var(--dash-card-min-h), auto);
  gap: var(--dash-gap);
  margin-bottom: 24px;
}

/* Same weight as .card, which is the app's one "this is a unit you read"
   outline — a dashboard card is not a new kind of object, it is a card with a
   heading and a footer pinned to its bottom edge. */
.dash-card {
  display: flex; flex-direction: column;
  background: var(--panel);
  border: var(--line) solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  /* The floor described at --dash-card-min-h. It is on the card rather than
     on the grid's rows so that the checklist band, which is a .dash-card
     outside the grid, is held to the same size as the cards below it. */
  min-height: var(--dash-card-min-h);
  transition: border-color .22s ease-out, background-color .22s ease-out, transform .22s ease-out, box-shadow .22s ease-out;
}
/* Same lift/glow/spotlight language as .cat-tile (issue #251), but a gentler
   tilt (motion.js caps --tilt-x/--tilt-y lower for these large panels — ~2deg
   vs ~6deg) since a dashboard card is wide enough that a 6deg tilt would read
   as a lurch rather than a polish. */
@media (hover: hover) and (pointer: fine) {
  .dash-card:hover {
    border-color: var(--border-strong);
    background: var(--panel-2);
    transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) translateY(-7px) scale(1.045);
    box-shadow: 0 16px 34px rgba(0, 0, 0, .5), 0 0 24px rgba(255, 255, 255, .18);
  }
}
/* A card with nothing in it yet states that quietly rather than competing with
   the cards that do have something to say. */
.dash-card--muted { border-style: dashed; }
.dash-card--wide { grid-column: 1 / -1; margin-bottom: var(--dash-gap); }
/* The community card is the one card holding a LIST rather than a single fact,
   so it takes the full width of the grid: the group rows get room for a name,
   a time and a message preview on one line, and the odd card out stops leaving
   an empty cell beside it. No margin — inside the grid, the gap already
   handles the spacing that .dash-card--wide has to add for itself above it. */
.dash-card[data-slot="community"] { grid-column: 1 / -1; }

.dash-card-title {
  font-size: 20px; line-height: 1.2;
  color: var(--text); margin: 0 0 12px;
}
/* Issue #271: a small red tick before every dashboard panel title — the
   dashboard's one small red touch beyond the progress bars it already had. */
.dash-card-title::before {
  content: "";
  display: inline-block;
  width: 3px; height: 14px;
  margin-right: 8px;
  vertical-align: -2px;
  background: var(--accent-red);
}
/* flex:1 is what keeps every footer on the same line as its neighbours'
   regardless of how much body text each card carries. */
.dash-card-body { flex: 1; }
.dash-card-foot { margin-top: 14px; }
.dash-card-foot .btn { width: 100%; text-align: center; }

.dash-headline { font-size: 15px; font-weight: 600; margin: 0 0 6px; overflow-wrap: anywhere; }
.dash-headline a { color: inherit; text-decoration: none; }
.dash-headline a:hover { text-decoration: underline; text-underline-offset: 3px; }
.dash-headline--quiet { color: var(--muted); }
.dash-sub { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; }
.dash-empty { color: var(--muted); font-size: 14px; margin: 0; }
.dash-when { color: var(--muted); font-size: 12px; }
/* "Completed" on the continue card: the module is still where you were, so
   the card still offers it — the flag is there so the offer isn't misread as
   unfinished work. */
.dash-flag {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  border: var(--line-sm) solid var(--border); border-radius: 999px;
  padding: 1px 8px; color: var(--muted);
}

/* The percentage is the one number on the page worth reading from across the
   room, so it is set at display size above its own bar. */
.dash-pct { font-family: var(--font-heading); font-size: 28px; font-weight: 600; letter-spacing: 0.01em; line-height: 1; margin-bottom: 10px; }
/* Issue #271: the % sign beside the big progress number is the dashboard's
   other small red touch. */
.dash-pct-sign { font-size: 16px; color: var(--accent-red); margin-left: 2px; }
.dash-card .progress { margin: 0 0 12px; }
.dash-stats { list-style: none; margin: 0; padding: 0; color: var(--muted); font-size: 14px; }
.dash-stats li { padding: 3px 0; }
.dash-stats strong { color: var(--text); }

.dash-person { display: flex; gap: 10px; align-items: flex-start; }
.dash-person .hint { margin: 2px 0; }
/* The rule .instructor-card used to carry, kept here for the same reason: the
   contact lines stay muted like the rest of .hint and only the underline says
   they are clickable, so the card doesn't turn into a wall of links. Since
   #195 this is the only copy of it. Without it these
   tel:/mailto: links take the browser's default link colour, which is the one
   colour this stylesheet does not use. */
.dash-person .hint a { color: inherit; text-decoration: underline; }

.dash-groups { list-style: none; margin: 0; padding: 0; }
.dash-group {
  display: grid; grid-template-columns: 1fr auto; gap: 2px 8px;
  padding: 8px 0; border-top: var(--line-sm) solid var(--border);
}
.dash-group:first-child { border-top: none; padding-top: 0; }
.dash-group-name { font-weight: 700; color: inherit; text-decoration: none; overflow-wrap: anywhere; }
.dash-group-name:hover { text-decoration: underline; text-underline-offset: 3px; }
/* The preview spans both columns under the name + timestamp row, and is
   clamped to two lines so one long post can't make its group's row four times
   the height of the others. */
.dash-group-msg {
  grid-column: 1 / -1; color: var(--muted); font-size: 13px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
.dash-group-msg strong { color: var(--text); font-weight: 600; }

/* My downloads (issue #217). Same two-column row as a community group — name
   on the left, "when" on the right, a muted second line spanning both — so the
   two list cards read as the same kind of list rather than as two designs. */
.dash-docs { list-style: none; margin: 0; padding: 0; }
.dash-doc {
  display: grid; grid-template-columns: 1fr auto; gap: 2px 8px;
  padding: 8px 0; border-top: var(--line-sm) solid var(--border);
}
.dash-doc:first-child { border-top: none; padding-top: 0; }
.dash-doc-name { font-weight: 700; color: inherit; text-decoration: none; overflow-wrap: anywhere; }
.dash-doc-name:hover { text-decoration: underline; text-underline-offset: 3px; }
.dash-doc-meta {
  grid-column: 1 / -1; color: var(--muted); font-size: 13px; overflow-wrap: anywhere;
}

/* The onboarding checklist (issue #182). Most of these rules were laid down
   with the slot in #181; what #182 adds is the box at the start of each row and
   the count above the list. */
.dash-checks { list-style: none; margin: 0; padding: 0; }
.dash-check {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0; border-top: var(--line-sm) solid var(--border);
}
.dash-check:first-child { border-top: none; }
/* Checklist steps are links, but the page is black-and-white: inherit the
   row's colour and let the underline carry the affordance, the same way the
   instructor card's contact lines do. */
.dash-check a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.dash-check.done { color: var(--muted); text-decoration: line-through; }
.dash-check.done a { color: inherit; }
/* The tick box. inline-flex rather than inline on purpose: an atomic inline box
   does not inherit the row's line-through, so a finished row strikes its label
   and leaves its tick legible. */
.dash-check-box {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: 18px; height: 18px;
  border: var(--line-sm) solid var(--border-strong); border-radius: 4px;
  font-size: 12px; line-height: 1;
}
.dash-check.done .dash-check-box { color: var(--text); }
/* How close the card is to leaving. The struck-through rows say WHICH steps are
   done; this says how many are left, which is what makes the card read as
   finite rather than as a permanent fixture of the page. */
.dash-check-count { margin: 0 0 8px; color: var(--muted); font-size: 13px; }

/* ---- Profile settings (issue #181) ----------------------------------------
   The account fields that used to live in a modal on the profile page. Full
   width inputs in a card, one per line: this is a form you fill in, not a row
   you skim. */
.settings-card { max-width: 560px; margin: 0 auto 16px; }
.settings-card label { display: block; margin-bottom: 12px; font-size: 14px; color: var(--muted); }
.settings-card input,
.settings-card textarea {
  display: block; width: 100%; margin-top: 4px; font: inherit;
  padding: 8px 10px; border-radius: 8px;
  border: var(--line) solid var(--border);
  background: var(--panel-2); color: var(--text);
}
.settings-card input:focus,
.settings-card textarea:focus { outline: none; border-color: var(--border-strong); }
.settings-card textarea { resize: vertical; }
.settings-card .btn-row { justify-content: center; }

/* ---- Branded loading overlay (issue #252) ---------------------------------
   Shown only when a route or its data takes longer than ~300ms to load;
   loading-overlay.js owns that timer and is the only thing that toggles
   .visible, so a fast page never flashes this. z-index 300 clears the modal
   layer (200): a slow load can start while a modal is open (e.g. "View as"
   fetching the account list), and the overlay should sit above it rather
   than behind it. The R is the same mark the sidebar and footer already use
   (issue #166), not a second copy of the logo. */
.loading-overlay {
  position: fixed; inset: 0; z-index: 300;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px;
  background: rgba(0, 0, 0, .72);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .2s ease, visibility .2s ease;
}
.loading-overlay.visible { opacity: 1; visibility: visible; pointer-events: auto; }

.loading-mark { position: relative; display: flex; align-items: center; justify-content: center; }
.loading-logo { height: 110px; width: auto; animation: loading-breathe 1.2s ease-in-out infinite; }

/* The light bar sweeps under the R rather than through it, so it reads as a
   floor the mark is hovering over. */
/* Issue #271: the sweep is red rather than white — the loading state's one
   small red touch. */
.loading-bar {
  position: absolute; left: 50%; bottom: -8px;
  width: 64px; height: 2px; margin-left: -32px;
  background: linear-gradient(90deg, transparent, var(--accent-red), transparent);
  animation: loading-sweep 1.2s ease-in-out infinite;
}

.loading-label {
  margin: 0; color: #fff; font-size: 12px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
}

@keyframes loading-breathe {
  0%, 100% { transform: scale(1) rotate(-4deg); opacity: .75; }
  50% { transform: scale(1.12) rotate(2deg); opacity: 1; }
}

@keyframes loading-sweep {
  0%, 100% { transform: translateX(-28px); opacity: 0; }
  50% { transform: translateX(28px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .loading-overlay { transition: none; }
  .loading-logo { animation: none; opacity: 1; }
  .loading-bar { display: none; }
}
