/* ===========================================================================
   THEMES -- Standard, Light and Dark
   ===========================================================================
   THE TOKENS ARE NAMED FOR THE JOB, NOT FOR THE COLOR, and that rename is the
   whole reason this file can carry three themes at all.

   Until this pass the palette was `--color-navy`, `--color-accent`,
   `--color-teal` and so on -- brand names -- and each of those three did two
   opposite jobs. `--color-navy` was the heading color 27 times AND the
   navbar's background 16 times. A dark theme needs the heading to get LIGHTER
   and the navbar to stay DARK, so there is no value you can give that one
   variable that is right for both. That is not a tidiness problem; it is what
   makes "just redefine the palette" impossible.

   So there are two layers now:

     LAYER 1, the brand constants. --brand-navy, --brand-gold, --brand-teal
     and their variants. THESE NEVER CHANGE BETWEEN THEMES. They exist so the
     product and the marketing site cannot drift, which is the same reason the
     favicon PNGs were copied byte-for-byte rather than re-exported. Nothing
     outside this block should reference them; they are raw material.

     LAYER 2, the role tokens. --color-heading, --color-chrome, --on-accent,
     --color-ring-outer and the rest. EVERY RULE IN THIS FILE USES ONLY THESE.
     A component written next year against them gets all three themes without
     its author thinking about themes, which is the property that makes this
     maintainable rather than merely done.

   THE RGB TRIPLETS ARE NOT REDUNDANT. `rgba()` cannot take a hex custom
   property, which is why 31 of this file's 58 rgba() literals used to be
   hand-inlined copies of palette colors. `rgba(var(--brand-navy-rgb), 0.08)`
   is the form that works, and it works in every browser that supports custom
   properties at all.

   WHY NOT color-mix(), WHICH WOULD BE TIDIER. It would be choosing a
   supported-browser floor as a side effect of a theming change, and this
   project has never stated one -- the learner-tour design recorded that gap
   first. The same argument rules out mask-image for the page textures below,
   which is why those are two asset variants rather than one recolored asset.

   STANDARD'S VALUES ARE EXACTLY WHAT THIS STYLESHEET RENDERED BEFORE THE
   SPLIT. Not approximately: the token names changed and no pixel did. That is
   asserted in tests_theming.py rather than promised here.

   TEN OF STANDARD'S PAIRINGS FALL SHORT OF WCAG 2.1 AA AND ARE PRESERVED
   ANYWAY, because "Standard unchanged" is the brief and these are colors the
   product already ships. They are not hidden: every one is listed in
   docs/accessibility-audit.md with its measured ratio. Light and Dark clear
   every one of the same checks. The worst is gold-as-text at 2.15:1 on the
   page background, which is why --color-accent-ink exists as a separate token
   from --color-accent-fill in the first place -- the two new themes darken it
   and Standard does not.

   HOW A THEME IS SELECTED. `data-theme` on <html>, server-rendered from
   User.theme by core.context_processors.theme, so it is in the markup before
   the first byte of <body> and there is no flash of the wrong theme and no
   inline script. "standard" needs no block of its own: it is what :root
   already says, which is also what any document rendered without the
   attribute gets.
   =========================================================================== */
:root {
  /* --- Layer 1: brand constants, identical in every theme --- */
  --brand-navy: #0b2545;
  --brand-navy-deep: #081b34;
  --brand-gold: #c9a227;
  --brand-gold-deep: #a3821e;
  --brand-teal: #14746f;
  --brand-teal-light: #1c948d;
  --brand-navy-rgb: 11, 37, 69;
  --brand-gold-rgb: 201, 162, 39;
  --brand-teal-rgb: 20, 116, 111;
  /* The security marking banner's colors. Constants beside the brand ones,
     and for the same reason the theme blocks do not repeat them: a marking
     does not change color with the theme. See .compliance-marking. */
  --brand-marking-paper: #ffffff;
  --brand-marking-ink: #000000;

  /* --- Non-color tokens: shared by every theme --- */
  --radius: 8px;
  --font-serif: Georgia, "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, serif;
  --font-sans: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* --- Layer 2: the Standard palette --- */
  color-scheme: light;

  /* Surfaces */
  --color-bg: #f6f1e4;
  --color-surface: #fffdf8;
  --color-surface-alt: #f1ece0;
  /* --color-chrome IS THE NAVBAR AND THE SKIP LINK, AND NOTHING ELSE.
     It is pinned to the brand navy in every theme because it is the one
     surface the white-and-gold lockup sits on, and that asset is a single
     file for all three themes -- which is why no new branding artwork was
     needed for any of this.

     --color-fill-strong IS EVERY OTHER SOLID DARK BLOCK: the module bar,
     table headers, stat tiles, the calendar's run bars and pinned markers,
     the selected tab, the chat launcher, the schedule buttons, back-to-top.
     They were all one token until 11 September 2026 and had to stop being,
     because in Dark the navy they shared measured 1.12:1 against the card
     behind them and 1.25:1 against the page.

     THAT IS THE SAME BUG THE MODULE BAR'S OWN COMMENT RECORDS FIXING, in
     the other direction. It read: a pale tint on a cream page, "barely two
     percent apart in luminance, so the bar dissolved into the background and
     the one question it exists to answer was invisible." Navy on navy is that
     sentence again. Standard had 15.14:1 for the same pairing, so nothing
     about it carried -- and no contrast assertion would have caught it, since
     the TEXT on these blocks is white and reads at 15:1 either way. It was
     found by looking at a screenshot. */
  --color-chrome: #0b2545;
  --color-chrome-hover: #081b34;
  --color-fill-strong: #0b2545;
  --color-fill-strong-hover: #081b34;
  --color-input-bg: #ffffff;
  --color-row-hover: #f7f9fa;
  --color-media-bg: #f2efe7;
  --color-media-bg-alt: #e8e3d8;
  /* Text */
  --color-text: #1b232c;
  --color-text-muted: #5a6672;
  --color-text-muted-rgb: 90, 102, 114;
  --color-heading: #0b2545;
  --color-link: #14746f;
  --color-link-hover: #a3821e;
  --color-on-chrome: #ffffff;
  --color-on-chrome-muted: #dfe7ea;
  --color-on-chrome-dim: #cdd7dc;
  --color-on-accent: #081b34;
  /* ONLY FOR THE TWO PLACES STANDARD PUTS WHITE ON GOLD -- the student-view
     preview bar and an empty unit emblem. That pairing is 2.42:1 and fails
     1.4.3; it is kept here because Standard is unchanged by instruction, and
     both new themes set this to the passing ink instead. */
  --color-on-accent-inverse: #ffffff;
  --color-on-secondary: #ffffff;
  /* Accent (gold) */
  --color-accent-fill: #c9a227;
  --color-accent-fill-hover: #a3821e;
  --color-accent-line: #c9a227;
  /* Gold that carries STATE rather than decoration -- the current calendar
     day, the selected icon, the current module step. 1.4.11 reaches these and
     not the decorative hairlines, so they are a separate token that the two
     new themes darken. */
  --color-accent-line-strong: #c9a227;
  --color-accent-ink: #c9a227;
  --color-accent-on-chrome: #c9a227;
  /* Secondary (teal) */
  --color-secondary: #14746f;
  --color-secondary-line: #14746f;
  --color-secondary-fill: #14746f;
  --color-secondary-on-chrome: #1c948d;
  /* Status */
  --color-danger: #b3261e;
  --color-danger-rgb: 179, 38, 30;
  --color-danger-strong: #a3342c;
  --color-success: #2e7d4f;
  --color-highlight: #fdf3d3;
  /* Lines and focus */
  --color-border: #e3dcc8;
  --color-border-strong: #0b2545;
  --color-ring-outer: #0b2545;
  --color-ring-inner: #fffdf8;
  /* Badges. The alpha is a token because a 12% wash that reads as a pill on
     cream produces nothing at all on a near-black page. */
  --badge-ink: #535e69;
  --badge-gold-ink: #745d15;
  --badge-teal-ink: #116460;
  --badge-navy-ink: #0b2545;
  /* Translucent fills. These replace 31 hand-inlined copies of palette colors
     -- rgba() cannot take a hex custom property, so every tint in this file
     used to spell its base color out again. The alpha is part of the token
     because a wash that reads as a pill on cream produces nothing at all on a
     near-black page: Dark raises every one of these. */
  --color-heading-rgb: 11, 37, 69;
  --color-secondary-line-rgb: 20, 116, 111;
  --tint-neutral: rgba(var(--color-text-muted-rgb), 0.12);
  --tint-accent: rgba(var(--brand-gold-rgb), 0.15);
  --tint-secondary: rgba(var(--color-secondary-line-rgb), 0.15);
  --tint-strong: rgba(var(--color-heading-rgb), 0.12);
  --tint-danger: rgba(var(--color-danger-rgb), 0.1);
  --color-chip-bg: rgba(var(--color-heading-rgb), 0.1);
  /* Effects */
  --shadow: 0 1px 3px rgba(var(--brand-navy-rgb), 0.08), 0 1px 2px rgba(var(--brand-navy-rgb), 0.06);
  --shadow-hover: 0 6px 16px rgba(var(--brand-navy-rgb), 0.14);
  --shadow-chrome: 0 2px 6px rgba(0, 0, 0, 0.15);
  --shadow-float: 0 2px 10px rgba(var(--brand-navy-rgb), 0.28);
  --shadow-menu: 0 6px 24px rgba(var(--brand-navy-rgb), 0.18);
  --glow-inset: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.12);
  --glow-inset-hover: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.2);
  --color-scrim: rgba(0, 0, 0, 0.55);
  /* Page decoration. --page-texture is the whole background-image list rather
     than a color, because a background-image cannot be recolored and
     mask-image is off the table (see the header note). */
  --color-page-glow: rgba(255, 255, 255, 0.55);
  --page-texture: url("img/page-wave.2a14f856e1f2.svg"), url("img/sketch-motif.f927341e331b.svg");
}

/* ---------- Light ---------- */
[data-theme="light"] {
  color-scheme: light;

  --color-bg: #eef1f5;
  --color-surface: #ffffff;
  --color-surface-alt: #e7ebf1;
  --color-chrome: #0b2545;
  --color-chrome-hover: #081b34;
  --color-fill-strong: #0b2545;
  --color-fill-strong-hover: #081b34;
  --color-input-bg: #ffffff;
  --color-row-hover: #f2f5f8;
  --color-media-bg: #e9edf2;
  --color-media-bg-alt: #dbe1e9;
  --color-text: #161b22;
  --color-text-muted: #515c68;
  --color-text-muted-rgb: 81, 92, 104;
  --color-heading: #0b2545;
  --color-link: #0f5f5b;
  --color-link-hover: #7d6212;
  --color-on-chrome: #ffffff;
  --color-on-chrome-muted: #dfe7ea;
  --color-on-chrome-dim: #cdd7dc;
  --color-on-accent: #081b34;
  --color-on-accent-inverse: #081b34;
  --color-on-secondary: #ffffff;
  --color-accent-fill: #c9a227;
  --color-accent-fill-hover: #a3821e;
  --color-accent-line: #c9a227;
  --color-accent-line-strong: #947417;
  --color-accent-ink: #6d570f;
  --color-accent-on-chrome: #c9a227;
  --color-secondary: #0f5f5b;
  --color-secondary-line: #14746f;
  --color-secondary-fill: #14746f;
  --color-secondary-on-chrome: #2aa89f;
  --color-danger: #b3261e;
  --color-danger-rgb: 179, 38, 30;
  --color-danger-strong: #a3342c;
  --color-success: #266942;
  --color-highlight: #fdf0c9;
  --color-border: #d4dbe3;
  --color-border-strong: #0b2545;
  --color-ring-outer: #0b2545;
  --color-ring-inner: #ffffff;
  --badge-ink: #4c5763;
  --badge-gold-ink: #6d570f;
  --badge-teal-ink: #0f5b57;
  --badge-navy-ink: #0b2545;
  --color-heading-rgb: 11, 37, 69;
  --color-secondary-line-rgb: 20, 116, 111;
  --tint-neutral: rgba(var(--color-text-muted-rgb), 0.12);
  --tint-accent: rgba(var(--brand-gold-rgb), 0.15);
  --tint-secondary: rgba(var(--color-secondary-line-rgb), 0.15);
  --tint-strong: rgba(var(--color-heading-rgb), 0.12);
  --tint-danger: rgba(var(--color-danger-rgb), 0.1);
  --color-chip-bg: rgba(var(--color-heading-rgb), 0.1);
  --shadow: 0 1px 3px rgba(var(--brand-navy-rgb), 0.10), 0 1px 2px rgba(var(--brand-navy-rgb), 0.07);
  --shadow-hover: 0 6px 16px rgba(var(--brand-navy-rgb), 0.16);
  --shadow-chrome: 0 2px 6px rgba(0, 0, 0, 0.15);
  --shadow-float: 0 2px 10px rgba(var(--brand-navy-rgb), 0.28);
  --shadow-menu: 0 6px 24px rgba(var(--brand-navy-rgb), 0.18);
  --glow-inset: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.12);
  --glow-inset-hover: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.2);
  --color-scrim: rgba(0, 0, 0, 0.55);
  --color-page-glow: rgba(255, 255, 255, 0.55);
  --page-texture: url("img/page-wave.2a14f856e1f2.svg"), url("img/sketch-motif.f927341e331b.svg");
}

/* ---------- Dark ----------
   NAVY CARRIED ALL THE WAY DOWN, not a neutral grey: #080f19 and #101c2b sit
   on the navy hue line, so the product still reads as itself rather than as a
   generic dark theme.

   THE NAVBAR KEEPS THE LITERAL BRAND NAVY and the page ground goes BELOW it.
   That is what lets the white-and-gold lockup image stay a single asset in all
   three themes -- no new branding artwork is needed for any of this.

   THE FOCUS RING INVERTS: gold outside, near-black inside. Gold was rejected
   for the light themes because it measures 2.15:1 on cream, and
   tests_focus_and_skip pins that fact. On #080f19 the same gold is 11.92:1 and
   is the best ring available. The same color, opposite verdicts -- which is
   exactly why none of the accessibility evidence could be assumed to carry
   between themes.

   ELEVATION IS NOT A DARKER SHADOW. Black at 2-18% does nothing on a dark
   ground, so depth here comes from --color-surface sitting above --color-bg
   and from --color-border, with the shadows raised to alphas that actually
   register against a lighter panel. This is the one place the three themes
   differ in technique rather than only in values.

   THIS BLOCK IS DUPLICATED BELOW, inside the prefers-color-scheme media query
   that serves the "Follow my device" choice. CSS has no way to reuse a
   declaration block across a media query, and the alternative -- a second
   layer of --dark-* variables aliased by 45 mapping lines -- duplicates the
   same number of lines while hiding the values. So the two are kept
   byte-identical and tests_theming.ThemeBlocksTest asserts it rather than
   trusting it. */
[data-theme="dark"] {
  color-scheme: dark;

  --color-bg: #080f19;
  --color-surface: #101c2b;
  --color-surface-alt: #16263a;
  --color-chrome: #0b2545;
  --color-chrome-hover: #123159;
  --color-fill-strong: #274563;
  --color-fill-strong-hover: #2e4e70;
  --color-input-bg: #0c1826;
  --color-row-hover: #17273a;
  --color-media-bg: #16263a;
  --color-media-bg-alt: #1f3146;
  --color-text: #e7edf4;
  --color-text-muted: #a7b6c8;
  --color-text-muted-rgb: 167, 182, 200;
  --color-heading: #cfe0f5;
  --color-link: #56c4bd;
  --color-link-hover: #e3c256;
  --color-on-chrome: #ffffff;
  --color-on-chrome-muted: #dfe7ea;
  --color-on-chrome-dim: #cdd7dc;
  --color-on-accent: #081b34;
  --color-on-accent-inverse: #081b34;
  --color-on-secondary: #081b34;
  --color-accent-fill: #c9a227;
  --color-accent-fill-hover: #dbb43f;
  --color-accent-line: #c9a227;
  --color-accent-line-strong: #c9a227;
  --color-accent-ink: #e3c256;
  --color-accent-on-chrome: #e3c256;
  --color-secondary: #56c4bd;
  --color-secondary-line: #3aa9a2;
  --color-secondary-fill: #3aa9a2;
  --color-secondary-on-chrome: #7fd6cf;
  --color-danger: #f08a82;
  --color-danger-rgb: 240, 138, 130;
  --color-danger-strong: #f5a09a;
  --color-success: #6ec48f;
  --color-highlight: #3b3520;
  --color-border: #2a3b52;
  --color-border-strong: #6d8bb0;
  --color-ring-outer: #f2c744;
  --color-ring-inner: #0b1420;
  --badge-ink: #c3d0de;
  --badge-gold-ink: #e3c256;
  --badge-teal-ink: #6fd3cc;
  --badge-navy-ink: #cfe0f5;
  --color-heading-rgb: 207, 224, 245;
  --color-secondary-line-rgb: 58, 169, 162;
  --tint-neutral: rgba(var(--color-text-muted-rgb), 0.16);
  --tint-accent: rgba(var(--brand-gold-rgb), 0.22);
  --tint-secondary: rgba(var(--color-secondary-line-rgb), 0.32);
  --tint-strong: rgba(var(--color-heading-rgb), 0.16);
  --tint-danger: rgba(var(--color-danger-rgb), 0.16);
  --color-chip-bg: rgba(var(--color-heading-rgb), 0.14);
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.55), 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-hover: 0 6px 20px rgba(0, 0, 0, 0.65);
  --shadow-chrome: 0 2px 6px rgba(0, 0, 0, 0.6);
  --shadow-float: 0 2px 10px rgba(0, 0, 0, 0.7);
  --shadow-menu: 0 6px 24px rgba(0, 0, 0, 0.7);
  --glow-inset: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.16);
  --glow-inset-hover: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.28);
  --color-scrim: rgba(0, 0, 0, 0.72);
  --color-page-glow: rgba(110, 150, 205, 0.12);
  --page-texture: url("img/page-wave-dark.3470349077d3.svg"), url("img/sketch-motif-dark.774a065fcd0f.svg");
}

/* ---------- "Follow my device" ----------
   A CHOICE, NOT THE DEFAULT. Standard is what every account gets until
   somebody picks something else -- see User.Theme -- and this applies only to
   an account that has explicitly asked to follow its device.

   Only the dark half is stated: `data-theme="auto"` with a light-preferring or
   undecided device already gets Standard from :root, which is the correct
   answer and needs no rule.

   Equal specificity to the blocks above (0,1,0), later in source order, so it
   wins when it matches. Keep it last. */
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] {
    color-scheme: dark;

    --color-bg: #080f19;
    --color-surface: #101c2b;
    --color-surface-alt: #16263a;
    --color-chrome: #0b2545;
    --color-chrome-hover: #123159;
    --color-fill-strong: #274563;
    --color-fill-strong-hover: #2e4e70;
    --color-input-bg: #0c1826;
    --color-row-hover: #17273a;
    --color-media-bg: #16263a;
    --color-media-bg-alt: #1f3146;
    --color-text: #e7edf4;
    --color-text-muted: #a7b6c8;
    --color-text-muted-rgb: 167, 182, 200;
    --color-heading: #cfe0f5;
    --color-link: #56c4bd;
    --color-link-hover: #e3c256;
    --color-on-chrome: #ffffff;
    --color-on-chrome-muted: #dfe7ea;
    --color-on-chrome-dim: #cdd7dc;
    --color-on-accent: #081b34;
    --color-on-accent-inverse: #081b34;
    --color-on-secondary: #081b34;
    --color-accent-fill: #c9a227;
    --color-accent-fill-hover: #dbb43f;
    --color-accent-line: #c9a227;
    --color-accent-line-strong: #c9a227;
    --color-accent-ink: #e3c256;
    --color-accent-on-chrome: #e3c256;
    --color-secondary: #56c4bd;
    --color-secondary-line: #3aa9a2;
    --color-secondary-fill: #3aa9a2;
    --color-secondary-on-chrome: #7fd6cf;
    --color-danger: #f08a82;
    --color-danger-rgb: 240, 138, 130;
    --color-danger-strong: #f5a09a;
    --color-success: #6ec48f;
    --color-highlight: #3b3520;
    --color-border: #2a3b52;
    --color-border-strong: #6d8bb0;
    --color-ring-outer: #f2c744;
    --color-ring-inner: #0b1420;
    --badge-ink: #c3d0de;
    --badge-gold-ink: #e3c256;
    --badge-teal-ink: #6fd3cc;
    --badge-navy-ink: #cfe0f5;
    --color-heading-rgb: 207, 224, 245;
    --color-secondary-line-rgb: 58, 169, 162;
    --tint-neutral: rgba(var(--color-text-muted-rgb), 0.16);
    --tint-accent: rgba(var(--brand-gold-rgb), 0.22);
    --tint-secondary: rgba(var(--color-secondary-line-rgb), 0.32);
    --tint-strong: rgba(var(--color-heading-rgb), 0.16);
    --tint-danger: rgba(var(--color-danger-rgb), 0.16);
    --color-chip-bg: rgba(var(--color-heading-rgb), 0.14);
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.55), 0 1px 2px rgba(0, 0, 0, 0.45);
    --shadow-hover: 0 6px 20px rgba(0, 0, 0, 0.65);
    --shadow-chrome: 0 2px 6px rgba(0, 0, 0, 0.6);
    --shadow-float: 0 2px 10px rgba(0, 0, 0, 0.7);
    --shadow-menu: 0 6px 24px rgba(0, 0, 0, 0.7);
    --glow-inset: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.16);
    --glow-inset-hover: inset 0 0 0 1px rgba(var(--brand-gold-rgb), 0.28);
    --color-scrim: rgba(0, 0, 0, 0.72);
    --color-page-glow: rgba(110, 150, 205, 0.12);
    --page-texture: url("img/page-wave-dark.3470349077d3.svg"), url("img/sketch-motif-dark.774a065fcd0f.svg");
  }
}

/* ---------- Paper ----------
   PAPER HAS NO THEME, AND WITHOUT THIS IT WOULD HAVE INHERITED ONE.

   The print blocks further down this file predate theming and each resets
   `body` to white on black, which was sufficient when there was one palette
   and is not now: every panel, card, table and certificate below `body` takes
   its own background from `--color-surface`. Printing a course in Dark would
   have put #101c2b rectangles on the page -- a black sheet with the content
   somewhere inside it, and a toner cartridge spent on it.

   This is a PRINT PALETTE rather than a fourth copy of Standard. The
   distinction is real: on screen, Standard's cream ground and cream borders
   are a deliberate warmth; on paper they are ink spent on nothing, and a
   border that reads at 1.35:1 on a monitor may not survive a laser printer at
   all. So the surfaces go to plain white, the borders go darker than any
   theme uses, and the ink goes to black.

   NAVY AND GOLD SURVIVE, because a certificate is meant to leave the building
   looking like the product -- `.certificate-frame` already asks for
   `print-color-adjust: exact` to keep exactly that.

   `:root, [data-theme]` so it reaches the explicit themes too: an attribute
   selector beats bare `:root`, so resetting only the latter would leave every
   chosen theme untouched -- which is every signed-in page.
   ------------------------------------------------------------------------ */
@media print {
  :root,
  [data-theme] {
    color-scheme: light;

    --color-bg: #ffffff;
    --color-surface: #ffffff;
    --color-surface-alt: #f2f2f2;
    --color-input-bg: #ffffff;
    --color-row-hover: #ffffff;
    --color-media-bg: #f2f2f2;
    --color-media-bg-alt: #e6e6e6;
    --color-text: #000000;
    --color-text-muted: #3d3d3d;
    --color-heading: #0b2545;
    --color-link: #0b3f3c;
    --color-link-hover: #0b3f3c;
    --color-on-chrome: #ffffff;
    --color-on-chrome-muted: #ffffff;
    --color-on-chrome-dim: #e8e8e8;
    --color-chrome: #0b2545;
    --color-chrome-hover: #081b34;
    --color-fill-strong: #0b2545;
    --color-fill-strong-hover: #081b34;
    --color-on-accent: #081b34;
    --color-on-accent-inverse: #081b34;
    --color-on-secondary: #ffffff;
    --color-accent-fill: #c9a227;
    --color-accent-fill-hover: #a3821e;
    --color-accent-line: #a3821e;
    --color-accent-line-strong: #947417;
    --color-accent-ink: #6d570f;
    --color-accent-on-chrome: #c9a227;
    --color-secondary: #0f5f5b;
    --color-secondary-line: #0f5f5b;
    --color-secondary-fill: #14746f;
    --color-secondary-on-chrome: #2aa89f;
    --color-danger: #8c1e18;
    --color-danger-rgb: 140, 30, 24;
    --color-danger-strong: #8c1e18;
    --color-success: #1f5c39;
    --color-highlight: #f5ecc8;
    --color-border: #999999;
    --color-border-strong: #000000;
    --color-ring-outer: #0b2545;
    --color-ring-inner: #ffffff;
    --color-heading-rgb: 11, 37, 69;
    --color-secondary-line-rgb: 15, 95, 91;
    --color-text-muted-rgb: 61, 61, 61;
    --badge-ink: #333333;
    --badge-gold-ink: #6d570f;
    --badge-teal-ink: #0f5b57;
    --badge-navy-ink: #0b2545;
    --tint-neutral: transparent;
    --tint-accent: transparent;
    --tint-secondary: transparent;
    --tint-strong: transparent;
    --tint-danger: transparent;
    --color-chip-bg: transparent;
    /* No shadows, no glow and no page texture on paper. A drop shadow is a
       grey smear at 600dpi and the line art is ink spent on decoration. */
    --shadow: none;
    --shadow-hover: none;
    --shadow-chrome: none;
    --shadow-float: none;
    --shadow-menu: none;
    --glow-inset: none;
    --glow-inset-hover: none;
    --color-scrim: transparent;
    --color-page-glow: transparent;
    --page-texture: none;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--color-text);
  line-height: 1.5;
  background-color: var(--color-bg);
  /* THE WAVE WAS A BASE64 DATA URI AND IS NOW A FILE, because there are two of
     it. The inline form saved a request when there was one ink color; with a
     dark variant it would have meant a second 1KB blob in this stylesheet that
     no reviewer could read or diff. img/page-wave.svg is byte-for-byte the
     same drawing the data URI carried. */
  background-image:
    radial-gradient(ellipse at top left, var(--color-page-glow), transparent 55%),
    var(--page-texture);
  background-repeat: no-repeat, repeat, repeat;
  background-size: 100% 600px, 400px 400px, 460px 460px;
}

a { color: var(--color-link); text-decoration: none; }
a:hover { color: var(--color-link-hover); text-decoration: underline; }

/* WCAG 1.4.1 (A), Use of Color. A link sitting INSIDE a run of body text has
   to be tellable from that text by something other than color -- teal on
   near-black is a color difference and nothing else, so a reader who cannot
   distinguish the two sees no link at all. Underline-on-hover does not help:
   hover is not available to a keyboard or touch user, and you have to already
   suspect the link is there to hover it.

   Scoped to prose rather than flipping the global rule above, because
   navigation, buttons, card titles and table cells are not "blocks of text" --
   axe-core only raises `link-in-text-block` for links with surrounding text,
   and underlining every link in the application would be a restyle rather than
   a fix. Found on the calendar's one-off download paragraph by the WCAG 2.1 AA
   scan, 6 September 2026; this rule covers that whole class, not that one link.

   `.btn` is excluded because a button-styled link is already distinguished by
   its box, and underlining its label just looks broken. */
p a:not(.btn):not(.badge) { text-decoration: underline; }

/* ---------------------------------------------------------------------------
   FOCUS INDICATOR -- WCAG 2.4.7 (AA, Focus Visible) and 1.4.11 (AA, Non-text
   Contrast).

   Until this rule, generic links and buttons had NO focus style at all and
   relied on the browser default. That default varies by browser and version,
   and this product paints a textured gradient page background and a navy
   navbar -- so what a keyboard user actually sees was unknown, and axe-core
   cannot decide it, because judging a ring against what is behind it is not
   mechanically decidable. Recorded as untested in docs/accessibility-audit.md
   §4.2 until it was measured and fixed.

   WHY NOT THE ACCENT COLOR, WHICH IS WHAT EVERY OTHER FOCUS RULE HERE USED.
   Measured, 7 September 2026, against the two backgrounds the product painted
   at the time:

     gold #c9a227 on --color-bg      #f6f1e4    2.15:1   FAILS 1.4.11
     gold #c9a227 on --color-surface #fffdf8    2.38:1   FAILS 1.4.11
     gold #c9a227 on the navy chrome #0b2545    6.36:1   passes

   Gold is a brand color chosen to sit on navy, and it does that well. As a
   focus ring on the light backgrounds that make up most of the product it is
   not an indicator at all -- it is a slightly warmer edge.

   TWO CONCENTRIC RINGS, so that whatever is behind the control, at least ONE
   of the two contrasts strongly with it. That "at least one" is the claim --
   not that both rings clear 3:1 everywhere, which they never did and never
   could: the dark ring on the navy navbar is 1.00:1 by construction, and the
   light ring is what carries that case.

   AND THE TWO RINGS SWAP ENDS IN DARK, WHICH IS THE WHOLE ARGUMENT FOR
   TOKENIZING THEM. In the light themes the outer ring is navy and the inner
   one is the surface color. In Dark the outer ring is GOLD -- the very color
   rejected above -- because on #080f19 that same #f2c744 measures 11.92:1.
   The identical color, opposite verdicts, decided entirely by what is behind
   it. Nothing about the September measurement could be assumed to carry, and
   this is the clearest case of why.

   Best of the two rings, per theme, against every ground a control sits on:

                        Standard   Light    Dark
     page ground         13.64     13.58    11.92   (outer / outer / outer)
     raised surface      15.14     15.39    10.65   (outer / outer / outer)
     sunken surface      13.05     12.86     9.49   (outer / outer / outer)
     the navy chrome     15.14     15.39     9.55   (INNER / INNER / outer)
     a gold button face   6.36      6.36     7.65   (outer / outer / INNER)
     a teal button face   5.50      5.59     6.50   (INNER / INNER / INNER)
     the two rings       15.14     15.39    11.48

   Neither ring depends on the background being known, which matters because
   the page background is a gradient plus two repeating textures: there is no
   single color behind a control to design against. A ring that is 13.6:1
   against the lightest point of that gradient is still far past 3:1 at its
   darkest.

   `outline` for the outer ring and `box-shadow` for the inner one, rather than
   two shadows: an outline is never clipped by an ancestor's `overflow`, so the
   dark ring -- the one carrying the common case -- survives being focused
   inside a scrolling panel or a card with hidden overflow.

   Modern browsers follow the element's own border-radius for the outline, so
   this fits a pill button and a square one without either being told about
   it. --------------------------------------------------------------------- */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
input[type="submit"]:focus-visible,
input[type="button"]:focus-visible {
  outline: 3px solid var(--color-ring-outer);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--color-ring-inner);
}

/* ---------------------------------------------------------------------------
   SKIP LINK -- WCAG 2.4.1 (A, Bypass Blocks).

   The landmark alone already satisfied 2.4.1: `<main>` is a bypass mechanism
   and a screen-reader user can jump to it directly. This exists because that
   argument covers exactly one of the two people it needs to.

   A SIGHTED KEYBOARD USER HAS NO LANDMARK NAVIGATION. They have Tab, and
   without this they spend it on every item in the navbar, on every page, all
   day. The landmark answers the success criterion; the link answers the
   problem the criterion is about.

   VISIBLE ON FOCUS RATHER THAN PERMANENTLY HIDDEN. `display: none` and
   `visibility: hidden` both remove an element from the tab order, which would
   make this a skip link nobody can reach -- the classic way to ship one that
   does nothing. Moving it off-screen with a transform keeps it focusable and
   keeps it in the accessibility tree.

   The transition is covered by the blanket `prefers-reduced-motion` block in
   animations.css, which zeroes `transition-duration` globally -- so somebody
   who has asked for reduced motion gets the link appearing instantly rather
   than sliding, with nothing extra written here.

   White on navy is 15.39:1, and it takes the same focus ring as everything
   else -- whose light inner ring is what shows against its own navy
   background. ------------------------------------------------------------ */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1000;
  padding: 0.7rem 1.25rem;
  background: var(--color-chrome);
  color: var(--color-on-chrome);
  font-family: var(--font-sans);
  font-weight: 700;
  text-decoration: none;
  border-bottom-right-radius: var(--radius);
  transform: translateY(-130%);
  transition: transform 0.15s ease-in-out;
}
.skip-link:focus {
  transform: translateY(0);
}
.skip-link:hover { color: var(--color-on-chrome); text-decoration: none; }

/* `<main>` takes focus when the skip link is followed, which needs
   `tabindex="-1"` in the markup -- without it the browser scrolls the document
   but leaves focus where it was, so the next Tab goes back into the navbar and
   the link appears to do nothing. Being focusable that way must not then draw
   a ring around the entire page, which is what the generic rule above would
   otherwise do to it. */
main:focus { outline: none; }

h1, h2, h3 { color: var(--color-heading); font-family: var(--font-serif); }

.rename-link {
  font-family: var(--font-sans);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--color-text-muted);
  vertical-align: middle;
  margin-left: 0.4rem;
}
.rename-link:hover { color: var(--color-link-hover); }

.page-nav {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.1rem;
}
.page-nav .back-link { margin-bottom: 0; }

.back-link {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--color-text-muted);
  margin-bottom: 0.75rem;
}
.back-link:hover { color: var(--color-link-hover); }

/* ---------- Nav bar ---------- */
.navbar {
  background: var(--color-chrome);
  color: var(--color-on-chrome);
  padding: 0.85rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-shadow: var(--shadow-chrome);
  border-top: 3px solid transparent;
  border-image: linear-gradient(90deg, var(--brand-gold-deep), var(--brand-gold), var(--brand-gold-deep)) 1;
}
/* THE BRAND IS THE LOCKUP IMAGE NOW -- see the note in base.html. The type
   rules below are kept rather than deleted: they are what the wordmark looked
   like as live text, they still describe the treatment baked into the image
   (serif, white, with the second half gold), and anything that ever renders
   `.brand` as text again -- a print sheet, an email template, a fallback --
   should look like the image rather than like default body copy.

   `inline-flex` so the image sits on the bar's center line rather than on a
   text baseline, which is what left a few pixels of gap under it. */
.navbar .brand {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-serif);
  font-size: 1.35rem;
  font-weight: 700;
  color: var(--color-on-chrome);
  letter-spacing: 0.02em;
}
.navbar .brand span { color: var(--color-accent-on-chrome); }
/* 40px, ARRIVED AT BY LOOKING RATHER THAN BY ARITHMETIC, and the difference
   between the two is worth recording.

   Matching ink height said 36px: the wordmark occupies 0.634 of the asset, so
   36px renders ~22.8px of ink against the 21.6px font-size the live text had.
   Rendered side by side that was wrong -- the old text was bold with
   letter-spacing, so equal ink height does not read as equal size. The
   comparison the eye actually makes is WIDTH, and there the wordmark went
   from 118px of text to ~78px inside a 36px lockup: a third narrower.

   40px puts it at ~86px and reads as a brand again rather than as another nav
   item. It is not a full restoration -- matching 118px would need a ~54px
   lockup and an ~84px bar, which is a fifth taller than this navbar has ever
   been, on every page. 40px is the point where the wordmark holds its own and
   the bar moves 63px -> 70px.

   The marketing site renders this same asset at 52px, in a header that is
   69px to begin with. Same image, different bar, so a different height is the
   thing that keeps them consistent rather than a departure from it.

   `height` with `width: auto` rather than both: the intrinsic asset is
   1237x429, and fixing one axis keeps it from being squashed if the asset is
   ever re-exported at another size. No media query -- at 40px the lockup is
   115px wide, still narrower than the 118px of text it replaced, so the bar
   wraps no sooner than it did before. */
.navbar .brand-logo {
  display: block;
  height: 40px;
  width: auto;
  flex: none;
}
/* Wraps rather than overflowing. A staff account carries enough nav entries to
   exceed a narrow viewport -- it did before the Units menu was added and more
   so after -- and `nowrap` turned that into a horizontally scrolling page
   rather than a second row. The gap tightens as space runs out so the wrap
   happens later. */
.navbar-links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem clamp(0.6rem, 1.6vw, 1.5rem);
  min-width: 0;
}
.navbar-links a { color: var(--color-on-chrome-muted); font-weight: 500; }
.navbar-links a:hover { color: var(--color-accent-on-chrome); text-decoration: none; }
.navbar-user {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--color-accent-on-chrome);
  font-weight: 600;
}
a.navbar-user:hover { color: var(--color-on-chrome); text-decoration: none; }
.user-avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--color-surface);
  /* LITERAL WHITE, AND IT STAYS LITERAL IN ALL THREE THEMES. This rim only
     ever appears on the navbar, and --color-chrome is the brand navy in
     Standard, Light and Dark alike -- so a translucent white edge is the
     right answer in every one of them. Tokenizing it would be a variable
     whose three values were identical. The .avatar-current rule below
     overrides it with --color-border for the one place the avatar is shown
     on a light page instead. */
  border: 1px solid rgba(255, 255, 255, 0.35);
}
.user-avatar--placeholder { border-color: rgba(255, 255, 255, 0.2); }
.avatar-current {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 0 1.25rem;
}
.avatar-current .user-avatar { width: 56px; height: 56px; border-color: var(--color-border); }
.avatar-clear-form { margin-top: 1.5rem; }
.navbar-role {
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-on-accent);
  background: var(--color-accent-fill);
  border-radius: 3px;
  padding: 0.1rem 0.35rem;
  vertical-align: middle;
}
.logout-form { display: inline; margin: 0; }
.logout-form button {
  background: none;
  border: none;
  color: var(--color-on-chrome-muted);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  padding: 0;
}
.logout-form button:hover { color: var(--color-accent-on-chrome); text-decoration: underline; }

/* ---------- Layout ---------- */
/* THE PAGE-TURN ANIMATION MUST NOT BE ABLE TO SCROLL THE DOCUMENT SIDEWAYS.
   `main.container` animates in with
   `perspective(1400px) rotateY(-5deg) translateX(-12px)` (animations.css). A
   transform does not change layout, but it DOES contribute to the scrollable
   overflow area -- so while that 280ms animation runs, the 3-D projection of
   the content area reaches past the right edge of the viewport and the
   document grows a horizontal scrollbar that then vanishes.

   MEASURED AT 768px, ON THREE PAGES: 22px of overflow, and `main.container`
   was the only element in the document wider than the viewport. Setting
   `animation: none` on it took the overflow to 0, which is what pins the cause
   to the animation rather than to any layout rule. Earlier work reported this
   three times as a static layout bug; it never was one.

   THE ROTATION IS THE CULPRIT, NOT THE 12px SHIFT -- which is worth writing
   down, because the shift is the part that looks guilty. Dropping the
   `translateX` alone makes it WORSE, 22px to 36px: the shift was moving the
   projected box leftward, and overflow off the left edge of an LTR document is
   not scrollable, so it was hiding part of the damage. Only the `rotateY`
   widens the box.

   BOTH SELECTORS ARE LOAD-BEARING. Measured, all four ways: `html` alone
   leaves 22px, `body` alone leaves 22px, and only the two together reach 0.
   This is the overflow-propagation rule and it is genuinely counter-intuitive.
   The root element hands its overflow to the viewport and is left `visible`
   itself; if the root is `visible`, the BODY's value is taken and propagated
   instead, leaving the body `visible` too -- so setting either one on its own
   just moves a value onto the viewport and leaves nothing in the document
   actually clipping. Setting `html` is what stops the body being the
   propagation source, which is what lets the body's own declaration survive
   and clip the transformed `main`. Deleting either line silently restores the
   bug, so neither is redundant.

   `clip` RATHER THAN `hidden`: both measured 0, but `hidden` makes a scroll
   container, which interferes with scroll anchoring and with any
   `position: sticky` added later. `clip` clips without establishing a
   scrollport. A browser too old to know `clip` ignores the declaration and
   gets today's brief scrollbar back -- a graceful way to lose, rather than the
   side effects `hidden` would hand it.

   Only the X axis. Vertical scrolling was checked explicitly and is untouched:
   on a page with 65px of scrollable height, it still scrolls the full 65 with
   this rule in place.

   This does not paper over genuine overflow. Wide content already carries its
   own scroll container -- see the `table` rule -- so nothing in this
   application relies on the document itself scrolling sideways. */
html, body { overflow-x: clip; }

.container { max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.page-header { margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 1rem; }
.page-header h1 { margin: 0 0 0.25rem; }
.page-header p { margin: 0; color: var(--color-text-muted); }
/* WRAPS. This is the single highest-impact responsive rule in the file: 69
   templates put their action buttons here, and without wrapping a page with
   four or five of them was 670px wider than a 400px viewport -- the heading and
   the buttons both pushed off-screen. Measured on course_detail, which had the
   worst of it. */
.page-header-actions { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; }

section + section { margin-top: 2.5rem; }

h2 {
  position: relative;
  padding-bottom: 0.6rem;
  margin-bottom: 1.25rem;
}
h2::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 64px;
  height: 6px;
  background-image: linear-gradient(var(--color-accent-line), var(--color-accent-line)),
    linear-gradient(var(--color-accent-line), var(--color-accent-line));
  background-size: 100% 1px, 100% 1px;
  background-position: top, bottom;
  background-repeat: no-repeat;
}

/* ---------- Cards / tile grid ---------- */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1.25rem;
  margin: 1.25rem 0;
}
/* Cards in a row stretch to the tallest, and the tallest is decided by its
   own content -- nothing here sets a fixed height. Actions go to the bottom
   so the button rows line up across cards of differing text length. */
.tile-grid > .card {
  display: flex;
  flex-direction: column;
}
.tile-grid > .card .card-actions { margin-top: auto; }
.card {
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-top: 3px solid var(--color-secondary-line);
  border-radius: var(--radius);
  box-shadow: var(--shadow), var(--glow-inset);
  padding: 1.25rem;
  transition: box-shadow 0.15s ease;
}
.card:hover { box-shadow: var(--shadow-hover), var(--glow-inset-hover); }
.card::before, .card::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  border: 1px solid var(--color-accent-line);
  opacity: 0.5;
  pointer-events: none;
}
.card::before { top: 7px; left: 7px; border-right: none; border-bottom: none; }
.card::after { bottom: 7px; right: 7px; border-left: none; border-top: none; }
.card h3 { margin: 0 0 0.4rem; font-size: 1.05rem; }
.card .card-meta { color: var(--color-text-muted); font-size: 0.88rem; margin-bottom: 0.75rem; }
.card .card-meta + .card-meta { margin-top: -0.5rem; }
.card .card-actions { display: flex; gap: 0.6rem 1rem; margin-top: 0.9rem; flex-wrap: wrap; }

/* ---------- Drag-to-reorder ---------- */
[data-reorderable] .card[data-id] { cursor: grab; }
[data-reorderable] .card[data-id]:active { cursor: grabbing; }
.card.reorder-dragging { opacity: 0.45; }
.card.reorder-drag-over-before,
.card.reorder-drag-over-after,
.card.reorder-drag-over-top,
.card.reorder-drag-over-bottom {
  position: relative;
}
.card.reorder-drag-over-before::before,
.card.reorder-drag-over-after::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--color-accent-line-strong);
  border-radius: 2px;
  box-shadow: 0 0 6px var(--color-accent-line-strong);
}
.card.reorder-drag-over-before::before { left: -0.625rem; }
.card.reorder-drag-over-after::after { right: -0.625rem; }
.card.reorder-drag-over-top::before,
.card.reorder-drag-over-bottom::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--color-accent-line-strong);
  border-radius: 2px;
  box-shadow: 0 0 6px var(--color-accent-line-strong);
}
.card.reorder-drag-over-top::before { top: -0.625rem; }
.card.reorder-drag-over-bottom::after { bottom: -0.625rem; }

/* ---------- Course dashboard grid (fixed-column 2D placement) ---------- */
.course-grid {
  display: grid;
  /* WAS `repeat(4, minmax(200px, 1fr))`, which is a hard floor of 4x200px plus
     gaps -- about 860px before the instructor dashboard, the landing page for
     every teaching account, started pushing tiles off-screen (measured 518px of
     overflow at 400px wide).

     `auto-fit` keeps four columns wherever four fit and drops to three, two and
     one as space runs out. The `min()` is what makes the last step work: a bare
     200px floor still overflows a viewport narrower than 200px plus padding,
     and `min(200px, 100%)` lets the track shrink below its floor rather than
     overflow. Same idiom as the landing pages' stat grid. */
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 1.25rem;
}
[data-course-reorder] .card[data-id] { cursor: grab; }
[data-course-reorder] .card[data-id]:active { cursor: grabbing; }
.grid-cell {
  /* A floor, not a ceiling: empty cells still read as drop targets, and a
     cell holding more content grows past this. */
  min-height: 190px;
  border-radius: var(--radius);
}
/* min-height rather than height: the card fills its cell, but content longer
   than the cell makes the card -- and so the grid row -- taller, instead of
   being squeezed into a fixed box. */
.grid-cell .card {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}
/* Pushed to the bottom so buttons line up across a row whose cards have
   different amounts of text above them. */
.grid-cell .card .card-actions { margin-top: auto; }

/* ---------- Dashboard tile heading ---------- */
/* A wrapping row rather than a column, so the "edit" link stays beside the
   name; the code is forced onto its own line by flex-basis below. */
.card h3.tile-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.5rem;
  margin-bottom: 0.5rem;
}
.tile-heading {
  display: inline-flex;
  align-items: center;
  gap: 0.1rem;
  min-width: 0;
}
.tile-name {
  font-size: 1.05rem;
  line-height: 1.3;
  /* long names wrap onto as many lines as they need rather than overflowing */
  overflow-wrap: anywhere;
}
.tile-code {
  /* full-width basis puts the code on its own line under the name */
  flex-basis: 100%;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.grid-cell--empty {
  border: 1px dashed var(--color-border);
}
.grid-cell--drag-over {
  outline: 2px dashed var(--color-accent-line-strong);
  outline-offset: -2px;
  background: var(--tint-accent);
}

/* ---------- Course icons ---------- */
.course-icon {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  object-fit: cover;
  vertical-align: middle;
  margin-right: 0.5rem;
  border: 1px solid var(--color-border);
}
.course-icon--placeholder {
  opacity: 0.85;
}

/* ---------- Icon picker (course form) ---------- */
.icon-picker-label {
  font-weight: 600;
  color: var(--color-heading);
  margin-bottom: 0.5rem;
}
.icon-picker-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}
.icon-option {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  padding: 0;
  background: var(--color-surface);
  border: 2px solid var(--color-border);
  border-radius: var(--radius);
  cursor: pointer;
}
.icon-option img {
  width: 32px;
  height: 32px;
  object-fit: cover;
  border-radius: 4px;
}
.icon-option-placeholder {
  font-size: 0.75rem;
  color: var(--color-text-muted);
}
.icon-option--selected {
  border-color: var(--color-accent-line-strong);
  box-shadow: 0 0 0 2px rgba(var(--brand-gold-rgb), 0.35);
}

/* ---------- Question editor ---------- */
.question-preview {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 0;
  font-size: 0.9rem;
  color: var(--color-text-muted);
}
.question-preview li { padding: 0.15rem 0; }
.question-preview--correct { color: var(--color-secondary); font-weight: 600; }
.formset-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.6rem;
}
.formset-row input[type="text"] { margin: 0; }
.formset-inline-label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.formset-arrow { color: var(--color-text-muted); }
.form-errors {
  list-style: none;
  padding: 0.75rem 1rem;
  margin: 0 0 1rem;
  background: var(--tint-danger);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius);
  color: var(--color-danger);
}
.quiz-section { margin-top: 2rem; }

/* ---------- Course days ---------- */
.course-day {
  border-left: 3px solid var(--color-border);
  padding-left: 1.1rem;
  margin-bottom: 2rem;
}
.course-day h2 { margin-bottom: 0.4rem; }
.course-day h3 { font-size: 1rem; margin: 1.1rem 0 0.5rem; }
.course-day-agenda { color: var(--color-text-muted); line-height: 1.6; }

/* ---------- Notifications ---------- */
.notification-bell {
  position: relative;
  display: inline-block;
  font-size: 1.15rem;
  line-height: 1;
  color: var(--color-on-chrome);
  text-decoration: none;
}
.notification-bell:hover { color: var(--color-accent-on-chrome); text-decoration: none; }
.notification-badge {
  position: absolute;
  top: -0.45rem;
  right: -0.6rem;
  min-width: 1.05rem;
  padding: 0 0.25rem;
  font-family: var(--font-sans);
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1.05rem;
  text-align: center;
  color: var(--color-on-accent);
  background: var(--color-accent-fill);
  border-radius: 999px;
}
.notification-list { margin: 1.25rem 0; }
.notification {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 3px solid transparent;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 0.9rem 1.1rem;
  margin-bottom: 0.75rem;
}
.notification--unread { border-left-color: var(--color-accent-line-strong); }
.notification-body { flex: 1 1 320px; }
.notification-message { margin: 0 0 0.2rem; }
.notification--unread .notification-message { font-weight: 600; }
.notification-actions { display: flex; align-items: center; gap: 0.5rem; }

/* ---------- Flash messages ---------- */
.messages {
  list-style: none;
  padding: 0;
  margin: 0 0 1.25rem;
}
.message {
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  margin-bottom: 0.5rem;
}
.message--error {
  border-color: var(--color-danger);
  background: var(--tint-danger);
  color: var(--color-danger);
}
.message--success {
  border-color: var(--color-secondary-line);
  background: var(--tint-secondary);
  color: var(--color-secondary);
}

.course-code {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: 0.72em;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--color-heading);
  background: var(--tint-accent);
  border-radius: 3px;
  padding: 0.1em 0.4em;
  margin-right: 0.45em;
  vertical-align: middle;
}

/* ---------- Course runs & assignment timing ---------- */
.run-summary {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
}
.run-summary h2 { font-size: 0.95rem; margin: 0.75rem 0 0.4rem; }
.run-summary h2:first-child { margin-top: 0; }
.run-summary ul { margin: 0 0 0.5rem 1.2rem; font-size: 0.9rem; }
.run-summary li { margin-bottom: 0.2rem; }

.timing-picker ul { list-style: none; padding: 0; margin: 0 0 0.5rem; }
.timing-picker li { margin-bottom: 0.25rem; }
.timing-picker label { font-weight: 400; }
.timing-option { padding-left: 1.4rem; }

/* ---------- Syllabus ---------- */
.syllabus-editor {
  background: var(--color-input-bg);
  min-height: 320px;
  margin-bottom: 1rem;
}
.syllabus-editor .ql-editor { min-height: 280px; font-size: 0.98rem; }

.syllabus-body {
  display: block;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 2rem 2.25rem;
  max-width: 820px;
  line-height: 1.65;
}
.syllabus-body h1,
.syllabus-body h2,
.syllabus-body h3,
.syllabus-body h4 {
  font-family: var(--font-serif);
  color: var(--color-heading);
  margin: 1.6rem 0 0.6rem;
}
.syllabus-body h1:first-child,
.syllabus-body h2:first-child,
.syllabus-body h3:first-child { margin-top: 0; }
.syllabus-body p { margin: 0 0 1rem; }
.syllabus-body ul,
.syllabus-body ol { margin: 0 0 1rem 1.4rem; }
.syllabus-body li { margin-bottom: 0.35rem; }
.syllabus-body blockquote {
  margin: 0 0 1rem;
  padding-left: 1rem;
  border-left: 3px solid var(--color-accent-line);
  color: var(--color-text-muted);
}
.syllabus-body pre,
.syllabus-body code {
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: 0.9rem;
}
.syllabus-body hr {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin: 1.5rem 0;
}

/* ---------- Step-by-step quiz ---------- */
.quiz-progress {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 1rem;
}
.reveal {
  border-left: 3px solid var(--color-border);
  padding: 0.25rem 0 0.25rem 1.1rem;
  margin-bottom: 1.5rem;
}
.reveal--correct { border-left-color: var(--color-secondary-line); }
.reveal--incorrect { border-left-color: var(--color-danger); }
.reveal-verdict {
  font-family: var(--font-serif);
  font-size: 1.25rem;
  font-weight: 700;
  margin: 0.4rem 0 0.75rem;
  color: var(--color-heading);
}
.reveal--correct .reveal-verdict { color: var(--color-secondary); }
.reveal--incorrect .reveal-verdict { color: var(--color-danger); }
.reveal-detail { margin: 0; }
.reveal-detail dt {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-top: 0.6rem;
}
.reveal-detail dd { margin: 0.15rem 0 0; }

/* ---------- Survey report ---------- */
.response-entry {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 0.75rem 1rem;
  margin-bottom: 0.6rem;
}
.response-entry summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--color-heading);
}
.response-entry summary .card-meta {
  display: inline;
  margin-left: 0.5rem;
  font-weight: 400;
}
.response-entry table { margin-top: 0.75rem; }

/* ---------- Grading & feedback ---------- */
.grade-form-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.grade-form textarea {
  width: 100%;
  min-width: 240px;
  margin: 0;
  font-size: 0.88rem;
}

.grade-result {
  border-left: 3px solid var(--color-accent-line);
  padding-left: 1rem;
  margin-bottom: 1.5rem;
}
.feedback-block { margin-top: 0.75rem; }
.feedback-label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.feedback-text {
  margin: 0.25rem 0 0;
  line-height: 1.6;
  white-space: normal;
}

/* ---------- Online course rendering ---------- */
.learn-layout {
  display: grid;
  grid-template-columns: minmax(12rem, 16rem) 1fr;
  gap: 2rem;
  align-items: start;
}
@media (max-width: 48rem) {
  .learn-layout { grid-template-columns: 1fr; }
}

.learn-nav-module h2 {
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 0.4rem;
}
.learn-nav-module ul {
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
}
.learn-nav-module li { margin-bottom: 0.3rem; }
.learn-nav-module a.is-current {
  font-weight: 700;
  border-left: 3px solid var(--color-accent-line-strong);
  padding-left: 0.5rem;
}

.learn-content .block { margin-bottom: 1.5rem; }
/* The reading measure, kept while the page around it got wider.
   course_learn.html opts into .container--wide, which takes the content column
   from roughly 780px to roughly 1260px. That width belongs to video, tables,
   embedded frames and the module bar; body text at 1260px is about 150
   characters a line, which is worse than what it replaced.

   Same rule and same number as .unit-block-text on the landing pages, which
   are the pages this convention came from -- they opt into the wide container
   and cap their own prose at 70ch. Only .block--text is capped: every other
   block type is something that benefits from the room. */
.learn-content .block--text { max-width: 70ch; }
.block--image img { max-width: 100%; height: auto; }
.block--image figcaption {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin-top: 0.35rem;
}

/* ---------- Transcript mode ---------- */
/* Purely presentational. Nothing here hides media: what transcript mode does
   is decline to *render* the tags at all, in the template, because a hidden
   <img> costs the same download as a visible one. These rules only mark the
   stand-in text as a stand-in, so a learner can tell the difference between
   "this is the whole block" and "something is missing". */
.transcript-toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: 0.35rem;
  font-size: 0.9rem;
}
.transcript-toggle-state { flex: 1 1 auto; }
.transcript-toggle button { width: auto; }

.block--transcript {
  border-left: 3px solid var(--color-border);
  padding-left: 0.75rem;
}
.transcript-label {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 0.25rem;
}
/* The alt text is the content here, not a footnote to it, so it is set as
   body copy rather than in the muted caption style. */
.transcript-alt { margin: 0 0 0.35rem; }
.block--transcript figcaption {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin-top: 0.35rem;
}
/* The inline stand-in for a popup: distinguished from the surrounding page
   the way the modal panel would have been, since it is still a digression
   rather than part of the main flow. */
.block--popup-inline {
  border: 1px solid var(--color-border);
  border-radius: 0.35rem;
  padding: 0.75rem 1rem;
}
.block--popup-inline h4 { margin-top: 0; }

/* ---------- Reviewer assignment ---------- */
.reviewer-form {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  margin: 0.5rem 0;
}
.reviewer-form label {
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0;
}
.reviewer-form select { margin: 0; font-size: 0.85rem; }

/* Same shape as the reviewer picker -- they sit together on every module
   card, and two controls that do the same kind of thing should not look like
   two different kinds of thing. */
.status-form {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  margin: 0.5rem 0;
}
.status-form label {
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0;
}
.status-form select { margin: 0; font-size: 0.85rem; }

/* ---------- Inline image metadata ---------- */
.inline-meta {
  display: grid;
  gap: 0.25rem;
  margin: 0.4rem 0 0.6rem;
}
.inline-meta label {
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0;
}
.inline-meta input { margin: 0; font-size: 0.88rem; }

.draft-actions {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  margin-left: 0.5rem;
}
.draft-actions form { display: inline; margin: 0; }

/* ---------- Page table of contents ---------- */
.page-toc {
  border-left: 3px solid var(--color-accent-line);
  padding: 0.6rem 0 0.6rem 1rem;
  margin-bottom: 1.75rem;
}
.page-toc h3 {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0 0 0.4rem;
}
.page-toc ol { margin: 0; padding-left: 1.1rem; }
.page-toc li { margin-bottom: 0.25rem; }
.toc-kind {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  margin-left: 0.4rem;
}
/* So an anchored jump does not tuck the block under the page heading. */
.block-anchor { scroll-margin-top: 1.5rem; }

/* ---------- Survey result charts ----------
   The SVG itself carries its own geometry and colors (see
   core/surveycharts.py, which reads the custom properties defined at :root, so
   the bars follow the theme like everything else). These rules only place the
   picture on the page.

   `max-width` rather than a fixed width: the chart is drawn on a 640-unit
   viewBox and scales to whatever column it lands in, so it stays readable in
   the narrow panel on the report and in the wider one on the printable page
   without a second set of geometry. */
.survey-chart-frame {
  margin: 0.75rem 0 1rem;
  max-width: 40rem;
  overflow-x: auto;
}
.survey-chart { display: block; height: auto; max-width: 100%; }

/* Printed, not dropped. A chart is the one part of this report that a table
   cannot restate compactly, and an SVG prints at the printer's resolution
   rather than the screen's -- which is the whole reason the charts are vector
   rather than generated images. */
@media print {
  .survey-chart-frame { break-inside: avoid; overflow: visible; }
}

/* ---------- Printable course ---------- */
.print-controls { margin-bottom: 2rem; }
.print-header { margin-bottom: 2rem; }
.print-module { margin-bottom: 2.5rem; }
.print-page { margin-bottom: 1.75rem; }
.print-figure img { max-width: 100%; height: auto; }
.print-note {
  border-left: 2px solid var(--color-text-muted);
  padding-left: 0.75rem;
  color: var(--color-text-muted);
}

@media print {
  /* Everything that is screen furniture, and nothing that is content. */
  .navbar,
  .page-nav,
  .print-controls,
  .sketch-defs,
  [data-ambient-gif] { display: none !important; }

  body { background: #fff; color: #000; }
  .container { max-width: none; margin: 0; padding: 0; }

  .print-module { page-break-inside: auto; }
  .print-page { page-break-inside: avoid; }
  h2, h3 { page-break-after: avoid; }
  .print-figure, .print-note { page-break-inside: avoid; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }
}

/* ---------- Authoring checks and bulk actions ---------- */
.bulk-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}
.bulk-select {
  display: block;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 0.4rem;
  cursor: pointer;
}

.check-group { margin-bottom: 1.75rem; }
.check-group h3 { margin-bottom: 0.4rem; }
.check-list { list-style: none; margin: 0; padding: 0; }
.check-item {
  border-left: 3px solid var(--color-danger-strong);
  padding: 0.5rem 0 0.5rem 0.9rem;
  margin-bottom: 0.8rem;
}
/* Unverified is information, not a fault, so it does not use the alarm color. */
.check-item--unverified { border-left-color: var(--color-text-muted); }

/* ---------- Course search, bookmarks, reading time ---------- */
.learn-search {
  display: flex;
  gap: 0.4rem;
  margin-bottom: 1.25rem;
}
.learn-search input { margin: 0; min-width: 0; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.nav-minutes {
  display: block;
  font-size: 0.68rem;
  color: var(--color-text-muted);
}

.search-results { list-style: none; margin: 1rem 0 0; padding: 0; }
.search-result {
  border-left: 3px solid var(--color-accent-line);
  padding: 0.5rem 0 0.5rem 0.9rem;
  margin-bottom: 1.1rem;
}
.search-snippet {
  margin: 0.35rem 0 0;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--color-text-muted);
}

.bookmark-form {
  margin: 0 0 1.5rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--color-border);
}
.bookmark-form textarea { width: 100%; max-width: 34rem; }

.bookmark-list { list-style: none; margin: 0; padding: 0; }
.bookmark {
  border-left: 3px solid var(--color-accent-line);
  padding: 0.6rem 0 0.6rem 0.9rem;
  margin-bottom: 1.1rem;
}
.bookmark-meta {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.bookmark-title {
  display: inline-block;
  font-weight: 700;
  margin: 0.2rem 0;
}
.bookmark-note {
  margin: 0.3rem 0 0;
  line-height: 1.6;
}

/* ---------- Embedded external pages ---------- */
.embed-frame {
  position: relative;
  /* 16:9, the shape most video and slide embeds expect */
  padding-top: 56.25%;
  background: var(--color-surface-alt);
  border-radius: 0.35rem;
  overflow: hidden;
}
.embed-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.embed-title {
  font-weight: 700;
  margin-bottom: 0.4rem;
}
.embed-fallback,
.embed-caption {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin: 0.5rem 0 0;
}

/* ---------- Module review comments ---------- */
.comment-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  max-width: 42rem;
}
.comment {
  border-left: 3px solid var(--color-accent-line);
  padding: 0.6rem 0 0.6rem 0.9rem;
  margin-bottom: 1rem;
}
/* Resolved comments stay readable but recede, so open ones lead the eye. */
.comment--resolved {
  border-left-color: var(--color-text-muted);
  opacity: 0.68;
}
.comment-meta {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.comment-body {
  margin: 0.3rem 0 0.5rem;
  line-height: 1.6;
}
.comment-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.comment-actions form { margin: 0; }

/* ---------- Inline knowledge checks ---------- */
.block--check {
  border-left: 3px solid var(--color-accent-line);
  padding: 1rem 1.1rem;
  background: var(--color-surface-alt);
  border-radius: 0 0.35rem 0.35rem 0;
}
.check-question { font-weight: 700; margin: 0 0 0.75rem; }

.check-choice {
  display: block;
  margin-bottom: 0.4rem;
  cursor: pointer;
}
.check-choice--wrong { color: var(--color-danger-strong); }
.check-choice--right { font-weight: 700; }

.check-verdict {
  font-weight: 700;
  margin: 0 0 0.6rem;
}
.check-verdict--right { color: var(--color-accent-ink); }
.check-verdict--wrong { color: var(--color-danger-strong); }

.check-preview {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0;
  font-size: 0.92rem;
}

/* ---------- Module branching map ---------- */
.map-canvas {
  overflow-x: auto;
  margin: 1rem 0 2rem;
  padding-bottom: 0.5rem;
}

.map-node-box {
  fill: var(--color-surface);
  stroke: var(--color-accent-line);
  stroke-width: 1.5;
}
.map-node-link { cursor: pointer; }
.map-node-link:hover .map-node-box {
  fill: var(--color-surface-alt);
  stroke-width: 2.5;
}
.map-node--orphan .map-node-box {
  stroke-dasharray: 5 4;
}

.map-node-order {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  fill: var(--color-text-muted);
}
.map-node-title {
  font-size: 0.95rem;
  font-weight: 700;
  fill: var(--color-text);
}
.map-node-meta {
  font-size: 0.7rem;
  fill: var(--color-text-muted);
}

.map-edge {
  fill: none;
  stroke: var(--color-accent-line);
  stroke-width: 1.6;
}
/* Backward jumps read as exceptions, so they are drawn as such. */
.map-edge--backward {
  stroke-dasharray: 6 4;
  opacity: 0.75;
}
.map-edge--self { stroke-dasharray: 3 3; }
.map-arrowhead { fill: var(--color-accent-line); }

.map-list { margin: 0.5rem 0 0 1.1rem; padding: 0; }
.map-list li { margin-bottom: 0.5rem; }
.map-list .card-meta { display: block; }

/* ---------- Student-view preview bar ---------- */
.preview-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
  padding: 0.6rem 1rem;
  border-radius: 0.35rem;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-inverse);
  font-size: 0.9rem;
}
.preview-bar form { margin: 0; }
/* THE BUTTON'S WASH STAYS LITERAL WHITE because it sits on gold, and gold is
   --color-accent-fill #c9a227 in all three themes. Its LABEL does not stay
   literal: white on gold is 2.42:1 and fails 1.4.3, which is what
   --color-on-accent-inverse exists to record. Standard keeps the failing
   white by instruction; Light and Dark both set that token to the dark ink,
   where the label clears 7.13:1 and the white wash beneath it only helps. */
.preview-bar button {
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.5);
  color: var(--color-on-accent-inverse);
  width: auto;
}

/* ---------- Progress through authored content ---------- */
/* A course's student site and Important Links. Deliberately plain: the
   external-link arrow already distinguishes these from internal navigation,
   so a second visual treatment on top of it would be saying the same thing
   twice. */
.course-links { margin-bottom: 1.5rem; }
.course-links-site { margin: 0 0 0.5rem; }
.course-links-list {
  margin: 0;
  padding-left: 1.2rem;
  /* Two columns where there is room, one where there is not -- a long list of
     short labels in a single column wastes most of a wide screen. */
  columns: 2 16rem;
  column-gap: 1.5rem;
}
.course-links-list li {
  /* Keeps a label and its arrow from being split across the column break. */
  break-inside: avoid;
  margin-bottom: 0.25rem;
}

/* ---------------------------------------------------------------------------
   External-link indicator
   ---------------------------------------------------------------------------
   Added by external_links.js to any link whose origin differs from this one.
   The glyph is an HTML entity like every other small icon here; the words
   beside it live in .sr-only and are what a screen reader actually gets.

   `inline-block` with no wrapping: the arrow must not be left stranded on a
   line of its own when the link text wraps, which is what a bare inline
   character would do at a narrow width.
--------------------------------------------------------------------------- */
.external-link-mark {
  display: inline-block;
  margin-left: 0.15em;
  white-space: nowrap;
}
.external-link-glyph {
  font-size: 0.8em;
  /* Lifted slightly: at 0.8em the arrow sits low against the baseline of the
     text it annotates. */
  vertical-align: 0.08em;
  /* Inherits the link's color rather than being given its own, so the mark
     reads as part of the link in every context this appears in -- body text,
     a button, a nav item, an inverted panel. A fixed color would be wrong in
     at least one of those. */
  color: inherit;
  /* Slightly recessive, so it annotates the link without competing with it.
     Not lower than this: it still has to be visible against the link color. */
  opacity: 0.75;
}
/* Somebody who has asked the OS for maximum contrast has already said they do
   not want subtle. */
@media (prefers-contrast: more) {
  .external-link-glyph { opacity: 1; }
}
/* An arrow saying "this goes somewhere else" means nothing on paper, where
   the URL is what matters and the reader cannot follow anything. */
@media print {
  .external-link-mark { display: none; }
}

/* ---------------------------------------------------------------------------
   Optional page backdrop
   ---------------------------------------------------------------------------
   THE SCRIM IS THE SAFETY NET, AND IT IS NOT OPTIONAL.
   An author picks a backdrop from the media library; nothing in this project
   can look at that image and judge whether text will survive on top of it --
   nothing measures its luminance (this project does not build on Pillow -- see
   core/media.py), and even if it did, a single average would say nothing about
   the corner the paragraph lands in.

   So legibility is not left to the author's eye. `::before` lays an opaque-
   enough wash of the page surface color over the whole image, and the
   content sits above it. Whatever the image -- black, white, a photograph, a
   busy diagram -- the effective backdrop behind the text is at least
   BACKDROP_SCRIM of the ordinary page color, so contrast cannot fall far
   below what plain content already has.

   0.88 is deliberately high and the image is deliberately faint as a result.
   That trade is the whole point: a backdrop is decoration, the text is the
   course, and a decoration that costs legibility has taken more than it gave.

   `prefers-contrast: more` takes it to fully opaque -- somebody who has asked
   the operating system for maximum contrast has already answered the question
   of whether they want decoration behind their text.
--------------------------------------------------------------------------- */
.learn-content--backdrop {
  position: relative;
  isolation: isolate;
}
.learn-content--backdrop::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--page-backdrop);
  background-size: cover;
  background-position: center;
  /* Fixed would detach the wash from the image while scrolling on iOS, which
     is the one place the scrim could slide off what it is protecting. */
  background-attachment: scroll;
  border-radius: var(--radius);
}
.learn-content--backdrop::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--color-bg);
  opacity: 0.88;
  border-radius: var(--radius);
}
/* Padding so the wash extends past the text rather than stopping at it. */
.learn-content--backdrop { padding: clamp(0.75rem, 2vw, 1.5rem); }

@media (prefers-contrast: more) {
  .learn-content--backdrop::after { opacity: 1; }
}
/* Printing a faded photograph behind body text wastes ink and helps nobody. */
@media print {
  .learn-content--backdrop::before,
  .learn-content--backdrop::after { display: none; }
}

/* The module bar: every module in authored order, state per module.
   WRAPS RATHER THAN SCROLLS. A course with twelve modules on a phone would
   otherwise need a horizontal swipe to see whether the last one is done,
   which is the one thing this bar exists to answer at a glance. `auto-fit` +
   `minmax` gives one row on a wide screen and as many as it needs on a
   narrow one, with no breakpoint to pick. */
.module-bar { margin-bottom: 0.9rem; }
.module-bar-track {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.module-bar-step { min-width: 0; }
/* NAVY, LIKE THE BANNER. These segments were a pale tint (#f2efe7) on a cream
   page (--color-bg: #f6f1e4) -- barely two percent apart in luminance, so the
   bar dissolved into the background and the one question it exists to answer
   was invisible. The banner's own navy is what a reader already recognizes as
   "this is the application talking", and white on it clears WCAG AAA.

   THE PALETTE VARIABLES, NOT THE HEXES THEY USED TO FALL BACK TO. Every color
   in the old rules named a custom property that does not exist at :root --
   --color-surface-muted, --color-highlight, --color-success, --color-gold --
   so each was silently rendering its hardcoded fallback. Those hexes were the
   drift this file's :root block exists to prevent. They are gone; what is left
   is the real palette. `#fff` stays literal because that is what .navbar and
   .brand already use for text on navy. */
.module-bar-step > a,
.module-bar-step > span {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  /* The state is carried by a thick top edge as well as by fill, so the three
     states stay distinguishable without relying on color alone. The fill is
     now one navy for every state, so the edge does more of that work than it
     did -- which is why each state also carries its own glyph in
     .module-bar-mark. Color, edge, and shape: three channels, and the
     screen-reader text in .sr-only is a fourth. */
  border-top: 4px solid var(--color-border);
  background: var(--color-fill-strong);
  color: var(--color-on-chrome);
  text-decoration: none;
  font-size: 0.8rem;
  line-height: 1.3;
}
/* Darker, not lighter. The old hover was a pale gold wash, which was legible
   under dark text and would put white text on cream. */
.module-bar-step > a:hover { background: var(--color-fill-strong-hover); }
.module-bar-step > a[aria-current="step"] {
  outline: 2px solid var(--color-accent-line);
  outline-offset: 1px;
}
.module-bar-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.module-bar-mark { flex-shrink: 0; font-weight: 700; }

/* The fill no longer varies by state, so the top edge and the glyph carry it.
   Both state colors are chosen to sit on navy rather than on cream: the old
   tints were mixed against the page and would vanish here. */
.module-bar-step--complete > a,
.module-bar-step--complete > span {
  border-top-color: var(--color-secondary-on-chrome);
}
.module-bar-step--complete .module-bar-mark { color: var(--color-secondary-on-chrome); }

.module-bar-step--in_progress > a,
.module-bar-step--in_progress > span {
  border-top-color: var(--color-accent-on-chrome);
}
.module-bar-step--in_progress .module-bar-mark { color: var(--color-accent-on-chrome); }

/* not_started keeps the neutral default above -- deliberately quiet, so the
   bar reads as "how far along am I" rather than as a wall of warnings. */

/* THE COMPLETION BAR MEASURES THE SAME AS THE MODULE BAR ABOVE IT.
 *
 * This carried `max-width: 32rem` while `.module-bar-track` is a grid filling
 * its container, so on the learning page the two stacked bars ended at
 * different places -- the completion bar stopped less than half way across,
 * under a module bar spanning the full width. They summarize the same number
 * (`completion.percent` draws both), so measuring differently made the figure
 * read as being about something narrower than the bar above it.
 *
 * THE CAP IS NOW OPT-IN RATHER THAN THE DEFAULT. It was doing real work in one
 * place -- the student dashboard, where the bar appears alone inside a
 * per-course card with no module bar to match -- and none anywhere else. So
 * `--narrow` carries it there and the base rule fills its container, which is
 * what every other page wants. Inverting it this way also left the learning
 * page's markup untouched, so the tests already pinning that page's chrome
 * kept testing exactly what they did before.
 */
.progress-summary { margin-bottom: 1.5rem; }
.progress-summary--narrow { max-width: 32rem; }

/* The author's own review progress in the builder.
 *
 * Reuses `.module-bar` and `.progress-bar` above -- "how far through am I"
 * should look the same wherever it is asked. What it does NOT reuse is their
 * data: every figure comes from `BuilderPageVisit` through
 * core/builderprogress.py, and none of the student progress system is
 * involved. See that module for why the two are kept apart.
 *
 * The heading is what stops the resemblance being a lie. A bar shaped exactly
 * like the student one, on a page about a course, would otherwise read as the
 * students' progress. */
.builder-progress {
  margin-bottom: 1.5rem;
  padding: 0.9rem 1.1rem;
  border: 1px dashed var(--color-border);
  border-radius: var(--radius);
}
.builder-progress-heading {
  margin: 0 0 0.6rem;
  font-size: 0.95rem;
  color: var(--color-heading);
}

/* ---------------------------------------------------------------------------
   Course calendar
   ---------------------------------------------------------------------------
   Two visual languages on one page, because a run and a session answer
   different questions: a run is a bar spanning the dates a course is on, a
   session is a marker on one day. See core/coursecalendar.py. */
/* ONE TOOLBAR ROW, so the grid starts near the top of the page.
   Title, zooms and paging shared three stacked blocks between them and pushed
   the calendar below the fold on an ordinary screen. They wrap to their own
   rows only when there is genuinely no width for them. */
.calendar-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.6rem;
}
.calendar-toolbar-title h1 { margin: 0; }
.calendar-range { margin: 0; color: var(--color-text-muted); font-size: 0.85rem; }
.calendar-paging { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.calendar-window-note {
  margin: 0 0 0.75rem;
  font-size: 0.78rem;
  color: var(--color-text-muted);
}
/* At an edge of the two-year window the control stays in place and stops being
   a link, rather than vanishing -- a row of buttons that changes width as you
   page is harder to use than one that grays out. */
.btn.is-disabled {
  opacity: 0.45;
  cursor: default;
  pointer-events: none;
}

.calendar-zooms {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0;
}
.calendar-zoom {
  padding: 0.35rem 0.75rem;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  text-decoration: none;
  font-size: 0.85rem;
}
.calendar-zoom--current {
  background: var(--color-fill-strong);
  border-color: var(--color-fill-strong);
  color: var(--color-on-chrome);
}
.calendar-zoom:hover { text-decoration: none; }

/* Below the grid now, and secondary to it: a key to what the cells contain
   rather than a queue in front of them. */
.calendar-runs-section { margin-top: 1.5rem; }
.calendar-runs-section h2 { font-size: 1.05rem; margin-bottom: 0.5rem; }
.calendar-runs { list-style: none; margin: 0 0 1.5rem; padding: 0; }
.calendar-run { margin-bottom: 0.4rem; }
/* Navy with a gold left edge, the same language `.schedule-button` uses for
   "this is a thing on this page, open it". */
.calendar-run-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.5rem 0.8rem;
  border-radius: var(--radius);
  border-left: 5px solid var(--color-accent-line);
  background: var(--color-fill-strong);
  color: var(--color-on-chrome);
  text-decoration: none;
}
.calendar-run-bar:hover { background: var(--color-fill-strong-hover); color: var(--color-on-chrome); }
.calendar-run-code { font-weight: 700; }
.calendar-run-name { flex: 1 1 12rem; min-width: 0; }
.calendar-run-dates {
  font-size: 0.78rem;
  color: var(--color-accent-on-chrome);
  white-space: nowrap;
}

/* `auto-fit` with a min, so the grid is seven-ish columns on a wide screen and
   as few as it needs on a phone -- the same technique `.module-bar-track` uses
   rather than a breakpoint to maintain. */
/* THE DOMINANT ELEMENT ON THE PAGE, and sized to be.
   Seven fixed columns for a month or a week, so the grid reads as a calendar
   -- weekdays line up under each other from row to row, which `auto-fit` could
   not guarantee. Below 48rem it falls back to as many columns as fit, because
   seven columns on a phone is seven unreadable slivers. */
.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.3rem;
}
.calendar-grid--day { grid-template-columns: minmax(0, 1fr); }

/* The weekday header. Same template and gap as the grid below it, in the same
   container, so the two line up without either knowing about the other. */
.calendar-weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.3rem;
  margin-bottom: 0.3rem;
}
.calendar-weekday {
  padding: 0.3rem 0.45rem;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-heading);
  border-bottom: 2px solid var(--color-accent-line);
}

@media (max-width: 48rem) {
  .calendar-grid { grid-template-columns: repeat(auto-fit, minmax(min(8.5rem, 100%), 1fr)); }
  /* The grid stops being seven columns here, so a seven-day header would name
     the wrong ones. Each cell already carries its own date and month. */
  .calendar-weekdays { display: none; }
}
/* Tall enough that the grid, not the chrome above it, is what fills the
   screen -- roughly six rows of a month at this height. */
.calendar-day {
  min-height: 7.5rem;
  padding: 0.45rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
}
.calendar-day--weekend { background: var(--color-surface-alt); }
/* Days from the months either side, padding a month out to whole weeks so the
   columns line up. Present so the grid reads as a calendar, dimmed so they are
   plainly not part of it, and never carrying sessions. */
.calendar-day--outside { opacity: 0.4; }
.calendar-day--today { outline: 2px solid var(--color-accent-line-strong); outline-offset: -2px; }
.calendar-day-number { font-weight: 700; font-size: 0.8rem; }
.calendar-day-month { font-weight: 400; color: var(--color-text-muted); }

/* MARKERS WRAP RATHER THAN TRUNCATE, and that is the actual fix for "a day
   with several courses on it".
 *
 * The CELL was never the problem: `min-height` is a floor and the grid's rows
 * are auto-sized, so a cell with twelve sessions already grew to 372px against
 * a 120px minimum with nothing clipped and nothing spilling -- measured, not
 * assumed. What WAS being cut off was each marker's own text, horizontally:
 * `white-space: nowrap` plus `text-overflow: ellipsis` meant a longer course
 * code became "COURSE-LONâ€¦" in a column roughly 250px wide.
 *
 * Since the cell grows anyway, that truncation bought nothing. Letting the
 * text wrap turns horizontal clipping into vertical growth the layout already
 * handles. `overflow-wrap: anywhere` so a single long unbroken code breaks
 * rather than pushing the column wide -- the same guard `.module-bar-name`
 * and `.schedule-button-text strong` already use. */
.calendar-marker {
  display: block;
  margin-top: 0.25rem;
  padding: 0.15rem 0.35rem;
  border-radius: 0.25rem;
  font-size: 0.72rem;
  line-height: 1.3;
  text-decoration: none;
  overflow-wrap: anywhere;
}
/* Two kinds of session, told apart by fill as well as by the word in the
   listing: a pinned day is something somebody scheduled, a recurring one is a
   weekly slot expanded from the course's run. */
.calendar-marker--pinned { background: var(--color-fill-strong); color: var(--color-on-chrome); }
.calendar-marker--recurring {
  background: var(--color-surface);
  border: 1px dashed var(--color-border-strong);
  color: var(--color-heading);
}
/* "+N more" -- a native disclosure, the same element the nav dropdowns and
   unit_images.html already use. Closed by default, so a crowded day keeps its
   row near the natural height until somebody asks to see the rest. */
.calendar-more { margin-top: 0.25rem; }
.calendar-more > summary {
  cursor: pointer;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--color-heading);
  padding: 0.1rem 0.35rem;
  border-radius: 0.25rem;
  list-style: none;
}
/* The default triangle is removed in both engines: the "+N" already says the
   control expands, and the marker at this size has no room for a glyph too. */
.calendar-more > summary::-webkit-details-marker { display: none; }
.calendar-more > summary:hover {
  background: var(--color-surface-alt);
}
/* Was `2px solid var(--color-accent)`, which measured 2.15:1 against the page
   background and 2.38:1 against a card -- both below 1.4.11's 3:1, on a
   control that sits on a light calendar cell. Now defers to the site-wide
   two-tone ring near the top of this file. The offset is kept small because a
   day cell is tight. */
.calendar-more > summary:focus-visible {
  outline: 3px solid var(--color-ring-outer);
  outline-offset: 1px;
  box-shadow: 0 0 0 2px var(--color-ring-inner);
}

/* "Start a course on this day". Quiet until the cell is hovered or the link is
   focused: it appears on every in-range cell for somebody who may create a
   course, and forty-two permanent buttons would compete with the content the
   grid exists to show. Focus-visible keeps it reachable by keyboard, where
   there is no hover to rely on. */
.calendar-add {
  display: block;
  margin-top: 0.25rem;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--color-heading);
  text-decoration: none;
  opacity: 0;
}
.calendar-day:hover .calendar-add,
.calendar-add:focus-visible { opacity: 1; }
@media (hover: none) {
  /* No hover to reveal it on a touch screen, so it stays visible there. */
  .calendar-add { opacity: 1; }
}

/* Scheduling actions on a run bar, outside the bar's own link so a click on
   one is not a click on the other. */
.calendar-run-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.3rem;
}

.calendar-session-list { padding-left: 1.1rem; }
.calendar-marker-kind {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------------
   Tabbed sections
   ---------------------------------------------------------------------------
   Every tab's label always visible, one panel shown at a time -- the one
   behavioral difference from an accordion, where several may be open at once.

   THE STRIP WRAPS RATHER THAN SCROLLS, the same choice `.module-bar-track`
   makes: a strip of eight tabs on a phone would otherwise need a sideways
   swipe to discover the last one, and a tab nobody can see is a section nobody
   reads. */
.tabs-block {
  margin: 1rem 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.tabs-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem;
  padding: 0.35rem 0.35rem 0;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}
.tabs-tab {
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  padding: 0.45rem 0.9rem;
  background: transparent;
  color: var(--color-heading);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}
.tabs-tab:hover { background: var(--color-surface-alt); }
/* The selected tab carries color AND a thick top edge, so the two states stay
   apart without relying on color alone -- the same three-channel reasoning
   `.module-bar-step` records. */
.tabs-tab--current {
  background: var(--color-fill-strong);
  border-top: 3px solid var(--color-accent-on-chrome);
  color: var(--color-on-chrome);
  font-weight: 700;
}
/* INSET, because a tab sits flush in a strip and an outward ring would
   overlap its neighbors. Two-tone like everything else, but drawn inward:
   light from the edge to 2px, navy from 2px to 4px.

   BOTH BANDS ARE NEEDED HERE, which is not obvious and is why the old rule was
   wrong. `.tabs-tab--current` is navy, so a keyboard user on the selected tab
   needs the light band; every OTHER tab is `background: transparent` over a
   `--color-surface` strip, so they need the dark one -- and arrow-key
   navigation lands on those constantly. The previous accent ring measured
   6.36:1 on the current tab and 2.38:1 on all the others, which is the wrong
   way round: it was legible only on the one tab you can already see you are
   on. */
.tabs-tab:focus-visible {
  outline: 2px solid var(--color-ring-outer);
  outline-offset: -2px;
  box-shadow: inset 0 0 0 2px var(--color-ring-inner);
}
.tabs-panel { padding: 0.9rem 1.1rem; }
/* Before tabs.js runs, every panel shows under its own heading and a divider
   keeps them apart. `--ready` is added once the script has taken over, at
   which point only one panel is ever visible and the divider would be a line
   under nothing. */
.tabs-panel + .tabs-panel { border-top: 1px solid var(--color-border); }
.tabs-block--ready .tabs-panel + .tabs-panel { border-top: none; }
.tabs-panel-heading { margin: 0 0 0.5rem; font-size: 0.95rem; }
/* The bar and its number on one line, so the figure reads as a label on the
   bar rather than as a separate fact further down the page. Wraps rather than
   squeezing the track to nothing on a narrow viewport. */
.progress-bar-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.progress-bar {
  flex: 1 1 12rem;
  /* Taller, and standing on something. The track was an 8%-black wash on a
     cream page with no edge, so an empty bar was invisible and a part-full one
     had no visible end to measure against. */
  height: 0.75rem;
  border-radius: 999px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  overflow: hidden;
}
.progress-bar-fill {
  display: block;
  height: 100%;
  background: var(--color-accent-fill);
  transition: width 0.3s ease;
}
/* The same number the bar is drawn from -- see course_learn.html, which passes
   `completion.percent` to both. Navy so it carries the weight of a heading
   rather than reading as a caption. */
.progress-bar-value {
  font-weight: 700;
  color: var(--color-heading);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
@media (prefers-reduced-motion: reduce) {
  .progress-bar-fill { transition: none; }
}

.nav-tally {
  display: block;
  font-size: 0.68rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--color-text-muted);
}
.nav-state {
  display: inline-block;
  width: 1em;
  text-align: center;
  color: var(--color-text-muted);
}
.nav-state--done { color: var(--color-accent-ink); font-weight: 700; }
.nav-state--started { color: var(--color-text-muted); }

.block--embed {
  border: 1px solid var(--color-border);
  border-radius: 0.4rem;
  padding: 1rem 1.1rem;
}
.block--embed h3 { margin: 0.35rem 0 0.5rem; }

/*
 * A wide table that scrolls inside its own box.
 *
 * The roster carries fourteen columns and cannot narrow usefully -- every one
 * is a distinct fact somebody came to read. Without this the PAGE scrolls
 * sideways instead, which pushes the status and notes controls off to the
 * right and makes them reachable only by dragging the whole layout. Same
 * arrangement as `.board-grid-wrap` on the roleplay board.
 */
.table-scroll {
  overflow-x: auto;
  max-width: 100%;
}

/*
 * The browser's own PDF viewer, embedded in the page.
 *
 * No library: `<object type="application/pdf">` hands the file to whatever the
 * browser already has. That means the height is ours to choose -- the element
 * has no intrinsic one, so without this it collapses to nothing. `85vh` keeps
 * the page header visible above it while giving the document most of the
 * screen; `min-height` stops a short window from squashing it to a letterbox.
 */
.pdf-frame {
  display: block;
  width: 100%;
  height: 85vh;
  min-height: 420px;
  border: 1px solid var(--color-border);
  border-radius: 0.4rem;
  background: var(--color-surface);
}

.embed-return { margin-top: 1.5rem; }

.outstanding-list {
  margin: 0.4rem 0 0.9rem 1.1rem;
  padding: 0;
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

.page-complete {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--color-border);
}

/* ---------- Content popups ---------- */
.popup-overlay {
  position: fixed;
  inset: 0;
  background: var(--color-scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 50;
}
/* The hidden attribute must win over display:flex. */
.popup-overlay[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   The first-time tour
   ---------------------------------------------------------------------------
   Overrides on the vendored driver.js build. The library file itself is never
   edited -- the whole point of static/core/vendor/ is that its contents are
   replaceable -- so everything that makes the tour look like Guidepost lives
   here. See static/core/tour.js and core/learnertour.py.

   THIS BLOCK HAS TO WIN A SPECIFICITY FIGHT, and that is not paranoia.
   driver.css is linked at the END of course_learn.html's body while this file
   is linked in the head, and stylesheet order is document order regardless of
   position -- so on a tie driver.css wins. Two answers to that, both used
   below:

     * The popover carries `tour-popover` as well, from tour.js's
       `popoverClass`. `.driver-popover.tour-popover` is two classes to the
       library's one, so it wins without `!important`.
     * The overlay cannot take an extra class -- the library gives no config
       for it -- and its z-index is an INLINE style (`svg.style.zIndex =
       "10000"`), which only `!important` can beat. So that one rule shouts,
       and it is the only one here that does.

   THE LADDER, WHICH IS THE REASON THE OVERLAY IS TOUCHED AT ALL. driver.js
   ships z-index 10000 on the overlay and 1000000000 on the popover, both of
   which sail over everything this stylesheet has carefully arranged. Brought
   back to 59 and 60:

     skip link      1000   stays above the tour, as it must
     tour popover     60   above its own overlay
     tour overlay     59   above the content popup, so a tour step could
                           explain one; above the launcher and back-to-top,
                           which are the two things a full-screen dim must not
                           leave floating on top of it
     popup overlay    50
     chat launcher    40   (and the nav dropdown panels)
     back to top      39

   NOTHING NEEDS RAISING TO BE HIGHLIGHTED. The library's overlay is one SVG
   with a hole cut in the path where the highlighted element is, so that
   element shows through at its own z-index rather than being lifted out of
   the stack. That is why there is no `.driver-active-element` rule here.

   MOTION IS ALREADY HANDLED. driver.css animates the overlay and popover with
   `animation: animate-fade-in var(--driver-animation-duration, .4s)`, without
   `!important`, so the blanket prefers-reduced-motion block at the foot of
   animations.css already zeroes it. Nothing is repeated here.
--------------------------------------------------------------------------- */
.driver-overlay { z-index: 59 !important; }

.driver-popover.tour-popover {
  z-index: 60;
  background-color: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-sans);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-hover);
  max-width: 22rem;
  padding: 1.1rem 1.25rem;
}
/* Serif headings, like every other heading on the site. */
.tour-popover .driver-popover-title {
  font-family: var(--font-serif);
  color: var(--color-heading);
  font-size: 1.05rem;
}
.tour-popover .driver-popover-description {
  color: var(--color-text);
  font-size: 0.92rem;
  line-height: 1.55;
}
.tour-popover .driver-popover-progress-text {
  color: var(--color-text-muted);
  font-size: 0.8rem;
}
/* THE CLOSE BUTTON IS INSET BY THE WIDTH OF THIS SITE'S FOCUS RING, and that
   is the whole reason this rule has geometry in it.

   driver.css pins it at `top: 0; right: 0` after an `all: unset`, so it sits
   flush in the corner. The site-wide indicator on `button:focus-visible` is
   `outline: 3px` at `outline-offset: 3px` plus a 3px inner shadow -- six
   pixels of ring outside the element -- and driver.js moves focus into the
   popover on every step, landing on this button first. Flush in the corner,
   that ring hung outside the panel and over the arrow: caught by looking at a
   screenshot, not by reading the CSS.

   6px of inset, to match. Not by removing the ring, which is the fix that
   would fail WCAG 2.4.7 -- and which this stylesheet's own focus-ring note
   explains was already got wrong once here. */
.tour-popover .driver-popover-close-btn {
  top: 6px;
  right: 6px;
  border-radius: 4px;
  color: var(--color-text-muted);
}
.tour-popover .driver-popover-close-btn:hover,
.tour-popover .driver-popover-close-btn:focus { color: var(--color-heading); }

/* .btn-secondary, in the library's own shape. It resets its buttons with
   `all: unset`, so every property has to be restated rather than inherited
   from the .btn rules further down this file. */
.tour-popover .driver-popover-footer-btn {
  background: transparent;
  color: var(--color-secondary);
  border: 1px solid var(--color-secondary-line);
  border-radius: 6px;
  padding: 0.32rem 0.75rem;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
}
.tour-popover .driver-popover-footer-btn:hover,
.tour-popover .driver-popover-footer-btn:focus {
  background: var(--color-secondary-fill);
  color: var(--color-on-secondary);
}
/* The last button is the one that moves you on, so it gets the primary
   treatment the page gives to Next and Mark complete. */
.tour-popover .driver-popover-navigation-btns button:last-child {
  background: var(--color-accent-fill);
  color: var(--color-on-accent);
  border-color: var(--color-accent-fill);
}
.tour-popover .driver-popover-navigation-btns button:last-child:hover,
.tour-popover .driver-popover-navigation-btns button:last-child:focus {
  background: var(--color-accent-fill-hover);
  border-color: var(--color-accent-fill-hover);
  color: var(--color-on-accent);
}

/* THE ARROW IS RECOLORED SIDE BY SIDE, NOT ALL AT ONCE. driver.css draws it
   as a 5px border box and makes three of the four sides transparent to leave a
   triangle -- so a single `border-color` here would fill the other three back
   in and produce a solid square. Each side rule below matches the one the
   library uses, at higher specificity. */
.tour-popover .driver-popover-arrow-side-left { border-left-color: var(--color-surface); }
.tour-popover .driver-popover-arrow-side-right { border-right-color: var(--color-surface); }
.tour-popover .driver-popover-arrow-side-top { border-top-color: var(--color-surface); }
.tour-popover .driver-popover-arrow-side-bottom { border-bottom-color: var(--color-surface); }

/* ---------------------------------------------------------------------------
   The rich-text editor
   ---------------------------------------------------------------------------
   Overrides on the vendored Quill "snow" build, for the same reason and by the
   same rule as the tour block above: `static/core/vendor/` is replaceable, so
   the library file is never edited and everything that makes the editor look
   like Guidepost lives here.

   WITHOUT THIS BLOCK THE EDITOR WAS A LIGHT PANEL IN A DARK THEME. quill.snow
   .css carries 48 colour literals across 15 distinct colours -- #ccc borders,
   #444 icons, a #fff dropdown, a #06c link blue -- none of which know anything
   about a theme. Seven authoring templates load it, so this is not a corner:
   it is most of what an author looks at while writing a course.

   THE `body` PREFIX IS THE SPECIFICITY FIGHT, AND IT IS DELIBERATE. Each of
   those templates links quill.snow.css from its own block, which is AFTER this
   file in document order, so on a tie the library wins. The tour block solves
   the same problem with an extra class, which the library's own config
   offers; Quill offers no such hook, and adding one would mean editing the
   vendored JS. A bare `body` costs one element's worth of specificity (0,0,1)
   and needs no markup at all, so `body .ql-snow .ql-stroke` beats
   `.ql-snow .ql-stroke` without `!important` anywhere.

   WHAT IS DELIBERATELY NOT OVERRIDDEN. `.ql-bg-*`, `.ql-color-*` and the
   colour-picker swatches are AUTHOR CONTENT -- the colours a course author
   chose for their own text. Theming those would rewrite what somebody wrote.
   `.ql-code-block-container` keeps its own dark scheme too: it is
   self-contained, legible in every theme, and is meant to read as a code
   block rather than as part of the page.
--------------------------------------------------------------------------- */
/* The frame. */
body .ql-toolbar.ql-snow,
body .ql-container.ql-snow { border-color: var(--color-border); }
body .ql-editor { color: var(--color-text); }
/* The placeholder was rgba(0,0,0,0.6) -- black at 60%, which on a dark editor
   surface is darker than the surface it sits on. */
body .ql-editor.ql-blank::before { color: var(--color-text-muted); }

/* Toolbar icons are SVG strokes and fills rather than type, so both have to be
   restated; a `color` alone leaves the icons at the library's #444. */
body .ql-snow .ql-stroke,
body .ql-snow .ql-stroke-miter { stroke: var(--color-text); }
body .ql-snow .ql-fill,
body .ql-snow .ql-stroke.ql-fill { fill: var(--color-text); }
body .ql-snow .ql-picker { color: var(--color-text); }

/* Active and hovered controls. The library's #06c is a blue chosen against
   white; the product already has a colour for "this is interactive". */
body .ql-snow.ql-toolbar button:hover,
body .ql-snow.ql-toolbar button:focus,
body .ql-snow.ql-toolbar button.ql-active,
body .ql-snow.ql-toolbar .ql-picker-label:hover,
body .ql-snow.ql-toolbar .ql-picker-label.ql-active,
body .ql-snow.ql-toolbar .ql-picker-item:hover,
body .ql-snow.ql-toolbar .ql-picker-item.ql-selected { color: var(--color-link); }
body .ql-snow.ql-toolbar button:hover .ql-stroke,
body .ql-snow.ql-toolbar button:focus .ql-stroke,
body .ql-snow.ql-toolbar button.ql-active .ql-stroke,
body .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
body .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
body .ql-snow.ql-toolbar button:hover .ql-stroke-miter,
body .ql-snow.ql-toolbar button.ql-active .ql-stroke-miter { stroke: var(--color-link); }
body .ql-snow.ql-toolbar button:hover .ql-fill,
body .ql-snow.ql-toolbar button:focus .ql-fill,
body .ql-snow.ql-toolbar button.ql-active .ql-fill,
body .ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
body .ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill { fill: var(--color-link); }

/* Dropdowns, which opened as a hardcoded white card. */
body .ql-snow .ql-picker-options {
  background-color: var(--color-surface);
  color: var(--color-text);
}
body .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label,
body .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
  border-color: var(--color-border);
}
body .ql-snow .ql-picker.ql-expanded .ql-picker-label { color: var(--color-text-muted); }
body .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill { fill: var(--color-text-muted); }
body .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke { stroke: var(--color-text-muted); }

/* The link tooltip: its own little panel, with its own white. */
body .ql-snow .ql-tooltip {
  background-color: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text);
  box-shadow: var(--shadow-hover);
}
body .ql-snow .ql-tooltip input[type="text"] {
  background: var(--color-input-bg);
  color: var(--color-text);
  border-color: var(--color-border);
}
body .ql-snow .ql-tooltip a.ql-action::after { border-right-color: var(--color-border); }
body .ql-snow a { color: var(--color-link); }

/* Editor content furniture. */
body .ql-snow .ql-editor blockquote { border-left-color: var(--color-border); }
body .ql-snow .ql-editor code { background-color: var(--color-surface-alt); }

/* ---------------------------------------------------------------------------
   Popup triggers
   ---------------------------------------------------------------------------
   `.popup-trigger` previously had NO rule at all: the trigger was a plain
   .btn and read as an ordinary button, with nothing to say it opens a modal
   or that it may be required to finish the page. Both presentations below
   carry the same two signals -- the dotted underline and the âŠ• marker -- so
   the affordance is recognizable whether the author chose a word or a button.

   Deliberately NOT styled like `a`: a link navigates away and this does not,
   and giving them the same appearance would promise the wrong thing.
--------------------------------------------------------------------------- */
.popup-trigger { cursor: pointer; font: inherit; }

/* The inline presentation: a phrase inside a sentence. Dotted underline
   rather than solid, which is the long-standing convention for "this reveals
   something in place" as against a link's "this takes you elsewhere". */
.popup-trigger--inline {
  display: inline;
  padding: 0 0.15em;
  border: 0;
  background: none;
  color: var(--color-heading);
  font-weight: 600;
  text-decoration: underline dotted currentColor;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 2px;
}
/* A leading marker, so the affordance survives for anyone who cannot see the
   underline -- Windows High Contrast drops decoration, and a dotted rule is
   the first thing to go at small sizes. */
.popup-trigger--inline::before {
  content: '\2295';
  margin-right: 0.25em;
  color: var(--color-accent-ink);
  font-weight: 700;
}
.popup-trigger--inline:hover,
.popup-trigger--inline:focus-visible {
  background: var(--color-highlight);
  border-radius: 3px;
}

/* The button presentation keeps .btn's shape and adds the same marker, so the
   two styles are recognizably the same kind of control. */
.popup-trigger--button::before {
  content: '\2295';
  margin-right: 0.4em;
  font-weight: 700;
}

/* Both get a visible focus ring: a required trigger a keyboard user cannot
   locate is a page they cannot finish -- which is why this one being the
   accent color mattered more than most. A popup trigger sits inline in prose
   on a light page, where gold measured 2.15:1 and the ring it drew was
   decorative rather than locating. Same two-tone treatment as everything else
   now. */
.popup-trigger:focus-visible {
  outline: 3px solid var(--color-ring-outer);
  outline-offset: 2px;
  box-shadow: 0 0 0 3px var(--color-ring-inner);
}

/* Once opened, the trigger stays usable -- re-reading is never blocked -- but
   stops shouting. The checkmark beside it is the positive signal. */
.popup-trigger--done { opacity: 0.85; }
.popup-trigger--done::before { color: var(--color-success); }

.popup-done {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  margin-left: 0.35em;
  color: var(--color-success);
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
}
.popup-done-mark { font-size: 1.05em; line-height: 1; }

@media (prefers-reduced-motion: no-preference) {
  .popup-trigger { transition: background-color 120ms ease; }
}

.popup-panel {
  position: relative;
  background: var(--color-surface);
  border-radius: 0.5rem;
  padding: 2rem 1.75rem 1.5rem;
  max-width: 40rem;
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
}
.popup-close {
  position: absolute;
  top: 0.5rem;
  right: 0.75rem;
  background: none;
  border: none;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  color: var(--color-text-muted);
  padding: 0.2rem 0.4rem;
  width: auto;
}
body.popup-open { overflow: hidden; }

/* ---------- Private staff notes ---------- */
.note-list {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0;
  max-width: 32rem;
}
.note-item {
  border-left: 3px solid var(--color-accent-line);
  padding: 0.4rem 0 0.4rem 0.75rem;
  margin-bottom: 0.6rem;
}
.note-meta {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.note-content {
  margin: 0.25rem 0 0.4rem;
  line-height: 1.6;
}
.note-empty {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin: 0 0 0.5rem;
}
.notice--staff {
  border-left: 3px solid var(--color-accent-line);
  background: var(--color-surface-alt);
  padding: 0.75rem 1rem;
  margin-bottom: 1.5rem;
  font-size: 0.9rem;
}

.award-form { display: inline-block; margin-top: 0.35rem; }
.revoke-form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.4rem;
}
.revoke-form input[type="text"] {
  margin: 0;
  font-size: 0.82rem;
  padding: 0.25rem 0.4rem;
  max-width: 190px;
  /* A FIXED BASIS, NOT THE FLEX DEFAULT. The only `.revoke-form` is in a cell
     of the fourteen-column roster, where an auto-layout table hands the column
     its minimum content width; with `flex: 0 1 auto` the input shrank to 15px
     and its "Reason (optional)" placeholder vanished. */
  flex: 0 0 9rem;
}

/* THE ROSTER'S PER-ROW CONTROLS KEEP THEIR OWN WIDTH.
   The global `select { width: 100% }` is right in a form card and wrong in a
   table cell: a percentage of a cell the table is shrinking to its minimum
   collapsed each status select to 22px, showing only its arrow. `width: auto`
   sizes it to its longest option and `flex: none` stops `td form`'s flex row
   from squeezing it again. Scoped to this form so no other page's dropdowns
   move. The table is wider as a result and scrolls further inside its
   `.table-scroll` box, which course_roster.html already provides for exactly
   this. */
.roster-status-form select { width: auto; flex: none; }
.roster-note-link { white-space: nowrap; }

/* The Compliance page's Override column, for the same reason: at 100% of the
   column the browser gave it, each select showed "Profile v". */
.compliance-override select { width: auto; }

/* The person page's Org placement table, the same again: at phone width the
   table shrank the Title column to its minimum and each select collapsed to
   31px, showing only its arrow. */
.placement-title select { width: auto; flex: none; }

.pending-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.prerequisite-picker ul {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 0;
  columns: 2;
}
.prerequisite-picker li { break-inside: avoid; }
.prerequisite-picker label { font-weight: 400; }

.badge--danger {
  background: var(--tint-danger);
  color: var(--color-danger);
}
.card-meta--warning { color: var(--color-danger); }

/* ---------- Roleplay board ---------- */
.board-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 15rem;
  gap: 1.25rem;
  align-items: start;
}
@media (max-width: 1000px) {
  .board-layout { grid-template-columns: 1fr; }
}

/* The grid can be wider than the page with several roles; scroll it inside
   its own box rather than letting the whole page scroll sideways. */
.board-grid-wrap { overflow-x: auto; }
.board-grid { width: 100%; border-collapse: collapse; }
.board-grid th,
.board-grid td {
  border: 1px solid var(--color-border);
  vertical-align: top;
  padding: 0.5rem;
}
/* The global `th` rule is navy-on-white for dark table headers. This grid
   wants light headers, so the color has to be restated as well as the
   background -- overriding only the background leaves white text on a pale
   surface, which is invisible rather than merely wrong. */
.board-grid th {
  background: var(--color-surface);
  color: var(--color-heading);
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.9rem;
}
/* Same reason: meta text inside a header would otherwise inherit #fff. */
.board-grid th .card-meta { color: var(--color-text-muted); }
.board-corner { width: 13rem; }
.board-session { width: 13rem; }
.board-session-label { font-weight: 700; color: var(--color-heading); }

.board-cell { min-width: 8rem; min-height: 3.5rem; background: var(--color-surface); }
.board-cell--empty { background: var(--color-surface-alt); }
/* Same feedback convention as the course grid: the target lights up, the
   thing being dragged fades. */
.board-cell--drag-over {
  outline: 2px dashed var(--color-secondary-line);
  outline-offset: -3px;
  background: var(--tint-secondary);
}

.person-chip {
  display: inline-block;
  margin: 0.15rem 0.15rem 0.15rem 0;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: var(--color-chip-bg);
  color: var(--color-heading);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: grab;
  user-select: none;
}
.person-chip:active { cursor: grabbing; }
.person-chip--dragging { opacity: 0.4; }

.board-pool {
  padding: 0.9rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
}
.board-pool h2 { margin-top: 0; font-size: 1.05rem; }
.pool-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem;
  min-height: 4rem;
  padding: 0.35rem;
  border: 1px dashed var(--color-border);
  border-radius: 6px;
}

.facilitator-form {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
  margin-top: 0.45rem;
}
.facilitator-form label {
  font-size: 0.65rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0;
}
.facilitator-form select { margin: 0; font-size: 0.8rem; max-width: 8rem; }

/* ---------- Calendar feed ---------- */
.feed-url {
  padding: 0.6rem 0.8rem;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  /* A subscription URL is long and has no spaces, so it must be allowed to
     break mid-token or it pushes the page sideways. */
  overflow-wrap: anywhere;
  font-size: 0.85rem;
}

/* ---------- Discussion board ---------- */
.topic-list { display: grid; gap: 0.75rem; margin-bottom: 1.5rem; }
.topic-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1.1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
}
.topic-main h2 { margin: 0 0 0.2rem; font-size: 1.1rem; }
.topic-main h2 a { text-decoration: none; }
.topic-count { text-align: center; min-width: 4rem; }
.topic-count-value {
  display: block;
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--color-secondary);
}

.discussion-thread { display: grid; gap: 0.9rem; margin-bottom: 1.5rem; }
.discussion-post {
  padding: 0.8rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  /* Transparent rather than absent, so the staff variant below swaps a
     color in without shifting the text by 3px. */
  border-left: 3px solid transparent;
  border-radius: 8px;
}

/* Staff input, made scannable in a long thread. Three cues rather than one:
   the navy edge, the tinted ground, and the role badge in the markup. Color
   alone would be invisible to a reader who cannot distinguish it, and a badge
   alone is easy to miss when skimming. */
.discussion-post--staff {
  border-left-color: var(--color-border-strong);
  background: linear-gradient(
    to right,
    var(--tint-strong),
    var(--color-surface) 40%
  );
}
.discussion-post--staff .discussion-author { color: var(--color-heading); }

.discussion-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.35rem;
}
.discussion-author { font-weight: 700; }
.discussion-role { text-transform: none; }
.discussion-body { font-size: 0.95rem; }
.discussion-body p { margin: 0 0 0.5rem; }
.discussion-body p:last-child { margin-bottom: 0; }

.discussion-reply { margin-top: 0.5rem; }
.discussion-reply summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--color-secondary);
}
.discussion-reply textarea { width: 100%; margin: 0.4rem 0; }

/* Indent, with a rule down the left so a reply chain reads as one. */
.discussion-replies {
  margin: 0.75rem 0 0 1.25rem;
  padding-left: 0.9rem;
  border-left: 2px solid var(--color-border);
  display: grid;
  gap: 0.75rem;
}
@media (max-width: 700px) {
  /* Three levels of indent is most of a phone screen; keep the rule, drop
     most of the offset. */
  .discussion-replies { margin-left: 0.35rem; padding-left: 0.6rem; }
}

/* ---------- AI assistant ---------- */
.chat-launcher {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 40;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1rem;
  border-radius: 999px;
  background: var(--color-fill-strong);
  color: var(--color-on-chrome);
  text-decoration: none;
  box-shadow: var(--shadow-float);
}
.chat-launcher:hover { background: var(--color-fill-strong-hover); }
.chat-launcher--blocked {
  background: var(--color-surface);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  max-width: 22rem;
  cursor: default;
}
.chat-launcher--blocked .chat-launcher-label {
  font-weight: 400;
  font-size: 0.8rem;
}
.chat-launcher-icon { font-size: 1.1rem; }
.chat-launcher-label { font-size: 0.9rem; font-weight: 600; }
.chat-launcher-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--color-accent-fill);
  color: var(--color-on-accent);
  font-size: 0.7rem;
  font-weight: 700;
}
@media print { .chat-launcher { display: none; } }

.chat-layout {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 1.25rem;
  align-items: start;
}
@media (max-width: 900px) {
  .chat-layout { grid-template-columns: 1fr; }
}
.chat-transcript {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-height: 60vh;
  overflow-y: auto;
  margin-bottom: 1rem;
}
.chat-message {
  padding: 0.6rem 0.9rem;
  border-radius: 10px;
  background: var(--color-surface-alt);
  border-left: 3px solid var(--color-border);
}
.chat-message--user { border-left-color: var(--color-secondary-line); }
.chat-message--assistant { border-left-color: var(--color-accent-line); }
.chat-role {
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.chat-body { white-space: pre-wrap; }
.chat-compose { display: grid; gap: 0.5rem; }
.chat-compose textarea { width: 100%; }
.chat-history { list-style: none; padding: 0; margin: 0 0 1rem; }
.chat-history li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--color-border);
}
.chat-history li.is-current { font-weight: 600; }
.chat-tools { list-style: none; padding: 0; margin: 0; font-size: 0.85rem; }
.chat-tools li { padding: 0.15rem 0; }

/* ---------- Certificate ---------- */
.certificate-actions {
  display: flex;
  gap: 0.75rem;
  margin: 0 0 1.25rem;
}

.certificate-sheet {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-hover);
  padding: 1rem;
  max-width: 1000px;
  margin: 0 auto;
}

/* Double rule: a gold hairline inset within a heavier navy frame. */
.certificate-frame {
  position: relative;
  border: 3px solid var(--color-border-strong);
  outline: 1px solid var(--color-accent-line);
  outline-offset: 6px;
  padding: 3rem 3.5rem 2.5rem;
  text-align: center;
}

.certificate-eyebrow {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--color-accent-ink);
  margin: 0 0 0.75rem;
}

.certificate-title {
  font-family: var(--font-serif);
  font-size: 2.6rem;
  line-height: 1.15;
  color: var(--color-heading);
  margin: 0 0 1.75rem;
}

.certificate-lede {
  font-family: var(--font-serif);
  font-style: italic;
  color: var(--color-text-muted);
  margin: 0 0 0.5rem;
}

.certificate-recipient {
  font-family: var(--font-serif);
  font-size: 2.1rem;
  color: var(--color-heading);
  margin: 0 0 1.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--color-accent-line);
  display: inline-block;
  min-width: 60%;
}

.certificate-course {
  font-family: var(--font-serif);
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--color-secondary);
  margin: 0 0 1.5rem;
}

.certificate-awarded {
  font-family: var(--font-sans);
  font-size: 0.95rem;
  color: var(--color-text);
  margin: 0 0 1rem;
}

.certificate-seal {
  display: flex;
  justify-content: center;
  margin: 0.5rem 0 1.5rem;
}

.certificate-seal .sketch {
  width: 88px;
  height: 116px;
  color: var(--color-accent-ink);
}

.certificate-footer {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 2rem;
  margin-top: 2rem;
  text-align: left;
}

.certificate-signature { flex: 0 1 320px; }
.certificate-signature-line {
  border-bottom: 1px solid var(--color-border-strong);
  height: 1.75rem;
}
.certificate-signature-name {
  font-family: var(--font-serif);
  font-weight: 700;
  color: var(--color-heading);
  margin-top: 0.35rem;
}
.certificate-signature-role {
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.certificate-verify { text-align: right; }
.certificate-verify-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--color-text-muted);
}
.certificate-code {
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: 0.8rem;
  color: var(--color-heading);
}

/* ---------- Print ----------
   A certificate is the one page meant to leave the app as paper, so the
   whole UI chrome is stripped and the sheet is laid out landscape. */
@media print {
  @page {
    size: landscape;
    margin: 12mm;
  }

  body {
    background: #fff !important;
    background-image: none !important;
  }

  /* app chrome that must never appear on the printed sheet */
  .navbar,
  .page-nav,
  .no-print,
  .certificate-actions,
  .notification-bell,
  .logout-form {
    display: none !important;
  }

  main.container {
    max-width: none;
    margin: 0;
    padding: 0;
    animation: none !important;
  }

  .certificate-sheet {
    border: 0;
    box-shadow: none;
    padding: 0;
    max-width: none;
  }

  .certificate-frame {
    padding: 2.5rem 3rem;
    /* keep the navy/gold rules and colored type in the PDF */
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    page-break-inside: avoid;
    break-inside: avoid;
  }

  .certificate-title { font-size: 2.3rem; }
  .certificate-recipient { font-size: 1.9rem; }
}

/* ---------- Instructor bio ---------- */
.instructor-bio {
  color: var(--color-text-muted);
  font-size: 0.88rem;
  line-height: 1.5;
  margin: 0.4rem 0 0;
  padding-left: 0.7rem;
  border-left: 2px solid var(--color-border);
}

/* ---------- Course catalog filters ---------- */
.catalog-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1rem 1.25rem;
  margin: 1.25rem 0;
}
.catalog-field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.catalog-field label {
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--color-heading);
}
.catalog-field input,
.catalog-field select { min-width: 190px; margin: 0; }
.catalog-field--actions {
  flex-direction: row;
  align-items: center;
  gap: 0.6rem;
}

/* ---------- Videos ---------- */
.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1.25rem;
  margin: 1rem 0;
}
.video-item {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1rem;
}
.video-title { margin: 0 0 0.35rem; font-size: 1rem; }
.video-description { color: var(--color-text-muted); font-size: 0.88rem; margin: 0 0 0.75rem; }
.video-player {
  width: 100%;
  max-height: 360px;
  border-radius: var(--radius);
  background: #000;
}
.video-embed {
  position: relative;
  width: 100%;
  padding-top: 56.25%;
  border-radius: var(--radius);
  overflow: hidden;
}
.video-embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
/* Shared attach sections (videos, instructional materials) */
.attach-section { margin-top: 2rem; }
.attach-form {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: 1rem;
}
.attach-form label { font-weight: 600; color: var(--color-heading); }
.attach-hint { margin-top: 0.75rem; font-size: 0.88rem; }

/* ---------- Stat tiles (dashboard KPIs) ---------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
  margin: 1.25rem 0;
}
.stat-tile {
  background: var(--color-fill-strong);
  color: var(--color-on-chrome);
  border-radius: var(--radius);
  padding: 1.1rem 1.25rem;
  box-shadow: var(--shadow);
}
.stat-tile .stat-value { font-size: 1.9rem; font-weight: 700; color: var(--color-accent-on-chrome); line-height: 1.2; }
.stat-tile .stat-label { font-size: 0.8rem; color: var(--color-on-chrome-dim); text-transform: uppercase; letter-spacing: 0.04em; }

/* ---------- Panels (forms) ---------- */
.panel {
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow), var(--glow-inset);
  padding: 1.75rem;
  max-width: 640px;
}
.panel::before, .panel::after {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  border: 1px solid var(--color-accent-line);
  opacity: 0.5;
  pointer-events: none;
}
.panel::before { top: 8px; left: 8px; border-right: none; border-bottom: none; }
.panel::after { bottom: 8px; right: 8px; border-left: none; border-top: none; }
.panel--narrow { max-width: 460px; }
.panel--center { margin: 2rem auto; text-align: center; }
.panel h1, .panel h2 { margin-top: 0; }

form p { margin: 0.9rem 0; }

fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem 1.25rem;
  margin: 0 0 1.25rem;
}
fieldset:last-of-type { margin-bottom: 0; }
legend { font-family: var(--font-serif); font-weight: 700; color: var(--color-heading); padding: 0 0.5rem; }

label { display: block; margin: 0.75rem 0 0.3rem; font-weight: 500; color: var(--color-text); }
.choice-option { display: flex; align-items: center; gap: 0.5rem; font-weight: 400; margin: 0.4rem 0; }
.choice-option input { width: auto; }
.match-row { display: flex; align-items: center; gap: 0.75rem; margin: 0.5rem 0; }
.match-row select { width: auto; flex: 1; max-width: 220px; }

input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
input[type="datetime-local"],
textarea,
select {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  font: inherit;
  background: var(--color-input-bg);
  color: var(--color-text);
}
input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--color-secondary-line);
  box-shadow: 0 0 0 3px var(--tint-secondary);
}

/* ---------- Buttons ---------- */
.btn, button, input[type="submit"] {
  display: inline-block;
  background: var(--color-accent-fill);
  color: var(--color-on-accent);
  border: none;
  border-radius: 6px;
  padding: 0.55rem 1.15rem;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease;
}
.btn:hover, button:hover, input[type="submit"]:hover { background: var(--color-accent-fill-hover); }
.btn-secondary { background: transparent; color: var(--color-secondary); border: 1px solid var(--color-secondary-line); }
.btn-secondary:hover { background: var(--color-secondary-fill); color: var(--color-on-secondary); }
.btn-sm { padding: 0.32rem 0.75rem; font-size: 0.85rem; }

/* ---------- Tables ---------- */
table {
  width: 100%;
  border-collapse: collapse;
  background: var(--color-surface);
  box-shadow: var(--shadow);
  border-radius: var(--radius);
  margin: 1rem 0;
  /* THE TABLE SCROLLS, NOT THE PAGE. 40 templates render a bare <table>, and a
     roster or library listing is simply wider than a narrow window -- at 400px
     the roster was dragging the whole document 169px sideways, taking the
     navbar and headings with it.
     `display: block` makes the table its own scroll container; the rows and
     cells keep their table display types, so an anonymous table box is
     generated inside and column alignment is unaffected. Measured against the
     unchanged rule at 1400px: identical width (1052px), so nothing about the
     desktop rendering moves.
     `width: max-content` was the other candidate and is rejected -- it shrank
     the same table to 814px on desktop.
     This replaces `overflow: hidden`, which existed only to clip the corners
     against the border radius; `overflow-x: auto` with `overflow-y: hidden`
     keeps that clipping while allowing the sideways scroll. */
  display: block;
  overflow-x: auto;
  overflow-y: hidden;
  max-width: 100%;
}
/* THE ROWS FILL THE BOX THE TABLE DRAWS. The `display: block` above has a cost
   the measurement at 1400px missed, because that table's content was already
   wider than its box: the anonymous table generated inside is sized to its
   content, not to the block, so a table with short content drew its white
   surface the full width and its header bar only as far as the columns reached
   -- 242px of 1052 on the rating detail page.
   Making the tbody itself the table box, at 100%, restores the stretch. Content
   wider than the box still overflows it and still scrolls inside the table.
   ONLY WHERE THERE IS NO <thead>. A thead and a tbody that each became their
   own table would size their columns independently, and headers would stop
   lining up with their cells. The bare `<tr><th>` tables this codebase writes
   almost everywhere get a single implicit tbody, so one table box and aligned
   columns. The tables with a thead -- compliance_settings.html's two -- are left
   exactly as they were. */
table:not(:has(> thead)) > tbody { display: table; width: 100%; }
/* LABEL/VALUE TABLES KEEP A NARROW LABEL COLUMN. The stretch above splits a
   two-column table's width between its columns, and in a table whose every row
   is a <th> label then one <td> value -- the person page's Account block,
   rating_detail.html's status block -- that turned the dark label column from
   149px into 442px. `width: 1%` with `nowrap` sizes the label column to its
   longest label and gives the rest to the values.
   `:has(+ td:last-child)` is what limits it to label/value rows: a header row's
   first <th> is followed by another <th>, and a data row starting with a <th>
   and carrying more than one <td> does not match.
   ONLY FROM 48rem UP. Below that `nowrap` makes a long label plus its value
   wider than a phone and the table starts scrolling sideways; there the
   ordinary wrapping split is the better layout. */
@media (min-width: 48rem) {
  table:not(:has(> thead)) > tbody > tr > th:first-child:has(+ td:last-child) { width: 1%; white-space: nowrap; }
}
th, td { padding: 0.65rem 0.85rem; text-align: left; border-bottom: 1px solid var(--color-border); }
th { background: var(--color-fill-strong); color: var(--color-on-chrome); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: var(--color-row-hover); }
td form { margin: 0; display: flex; gap: 0.5rem; align-items: center; }
td form input[type="number"] { width: 6rem; }

/* ---------- Ratings: the rating form and its rubric ----------
   docs/ratings-manager-design.md §2.9. Built on the ordinary table rules
   above -- surface, border, header fill -- rather than a second visual
   language, so the form reads as part of the product and not as a pasted
   document. Palette variables only, so every theme gets it for free. */
.rating-info th[scope="row"],
.rating-total th[scope="row"] {
  width: 14rem;
  background: var(--color-surface-alt);
  color: var(--color-heading);
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.9rem;
  vertical-align: top;
}
.rating-info td { vertical-align: top; }
.rating-info td textarea,
.rating-info td input[type="email"] { width: 100%; }
.rating-info td select { min-width: 16rem; }
.rating-attachments { margin: 0 0 0.5rem; padding-left: 1.1rem; }
.rating-attachments li { display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; align-items: baseline; }
.rating-override-detail { margin-top: 0.5rem; }
.rating-override-detail p { margin: 0.4rem 0; }

.rubric th[scope="row"].rubric-criterion {
  background: var(--color-surface-alt);
  color: var(--color-heading);
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.95rem;
  min-width: 10rem;
  vertical-align: top;
}
/* A real table box, so a grid with few or short criteria still spans the
   panel. The global `table { display: block }` exists so a bare table scrolls;
   the rubric already sits in a `.table-scroll`, which does that job. */
.table-scroll > table.rubric { display: table; width: 100%; }
.rubric .rubric-level { min-width: 11rem; }
.rubric .rubric-level-value {
  display: inline-block;
  min-width: 1.6rem;
  margin-right: 0.25rem;
  text-align: center;
  border-radius: 999px;
  background: var(--tint-strong);
  color: var(--color-on-chrome);
}
.rubric td { vertical-align: top; }
.rubric-cell { transition: background-color 0.15s ease; }
.rubric-cell.is-chosen { background: var(--tint-accent); box-shadow: inset 3px 0 0 var(--color-link); }
.rubric-choice { display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; }
.rubric-choice input[type="radio"] { flex: none; margin-top: 0.2rem; }
.rubric-choice-level { font-weight: 600; color: var(--color-heading); }
.rubric-rated { display: block; margin-bottom: 0.25rem; }
.rubric-description { color: var(--color-text); font-size: 0.9rem; line-height: 1.4; }
.rubric-comment { width: 24%; min-width: 14rem; }
.rubric-comment textarea { width: 100%; min-height: 4.5rem; }
.rubric--editor td textarea,
.rubric--editor th input[type="text"] { width: 100%; }

.rating-total { max-width: 34rem; }
.rating-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
  margin: 1.25rem 0 0.5rem;
  padding: 0.85rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
.rating-certify { display: inline-flex; align-items: center; gap: 0.45rem; font-weight: 600; }
.rating-result { font-weight: 600; }
.rating-result .card-meta { font-weight: 400; }

@media (max-width: 48rem) {
  /* A label column beside a field column leaves the field too little room on
     a phone -- inputs ran past the edge of the table. Below this width each
     row stacks: the label above its field, both the full width. The rubric is
     NOT restacked: criteria against levels is the point of it, so it keeps
     scrolling sideways inside its own `.table-scroll`. */
  .rating-info, .rating-info tbody, .rating-info tr,
  .rating-info th[scope="row"], .rating-info td,
  .rating-total, .rating-total tbody, .rating-total tr,
  .rating-total th[scope="row"], .rating-total td {
    display: block;
    width: auto;
  }
  .rating-info th[scope="row"], .rating-total th[scope="row"] { padding-bottom: 0.25rem; }
  .rating-info td, .rating-total td { padding-top: 0.25rem; }
  .rating-info td select { min-width: 0; width: 100%; }
  .rating-total { max-width: none; }
}

/* ---------- Misc ---------- */
.empty-state {
  color: var(--color-text-muted);
  font-style: italic;
  padding: 1.5rem;
  text-align: center;
  background: var(--color-surface);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius);
}
.badge {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--tint-neutral);
  /* WCAG 1.4.3 (AA). Badge text is 12px at weight 600, which is NOT "large
     text" -- that starts at 18.66px bold -- so it needs 4.5:1, measured
     against the tint composited over whatever sits behind it.
     var(--color-text-muted) gave 4.91:1 over --color-surface but only 4.46:1
     over --color-bg, so the same badge passed on a card and failed on the
     page. Measured with axe-core, 6 September 2026.

     THESE WERE LITERAL HEXES AND ARE NOW PER-THEME TOKENS, for the same
     reason they were literal in the first place. The original note said the
     fix belongs on the badge rather than on the palette, because it is the
     tinted background that eats the contrast -- that is still exactly right,
     and it is why there is a --badge-ink rather than a reuse of
     --color-text-muted. What changed is that there are three tinted
     backgrounds now, so the ink has to be three values.

     THE ALPHA IS ALSO PER-THEME. A 12% wash that reads as a pill on cream
     produces nothing at all over a near-black page, so Dark raises it to 16%
     (base), 22% (gold) and 32% (teal). Raising the wash changes what the ink
     is measured against, which is why the two move together.

     Re-derived and measured for all three, ink over tint over BOTH grounds:

                      Standard        Light          Dark
       .badge         5.01 / 5.54    5.50 / 6.18    9.26 / 7.96
       .badge--gold   5.03 / 5.51    5.50 / 6.16    7.77 / 6.76
       .badge--teal   5.03 / 5.54    5.70 / 6.40    6.39 / 5.59

     Standard's numbers are unchanged from 6 September: same colors, new
     names. Asserted per theme in tests_theming.BadgeContrastTest. */
  color: var(--badge-ink);
  margin: 0 0.35rem 0.35rem 0;
}
/* Was 3.18:1 over --color-surface and 2.90:1 over --color-bg -- the worst in
   the set, and what the scan reported first. Now 5.52 / 5.04. */
.badge--gold { background: var(--tint-accent); color: var(--badge-gold-ink); }
/* Recurring task cadence. Deliberately quiet -- it is an ongoing fact about
   the task, not a state change worth competing with Overdue for attention. */
.badge--repeat { background: var(--tint-strong); color: var(--badge-navy-ink); }

/* Grid/List switch on the shared libraries. The two buttons sit flush so they
   read as one control with a selected half, rather than two separate links. */
.view-toggle {
  display: inline-flex;
  gap: 0;
}
.view-toggle .btn { border-radius: 0; }
.view-toggle .btn:first-child { border-radius: 6px 0 0 6px; }
.view-toggle .btn:last-child { border-radius: 0 6px 6px 0; }

/* Sortable column headings: a link in a header should not look like body
   copy, and the arrow needs room not to reflow the row when it appears. */
table th a {
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
table th a:hover { text-decoration: underline; }

/* Private 1:1 messaging. Sides are set by a modifier class the template adds
   by comparing sender to viewer, so one stylesheet serves both people. */
.message-thread {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}
.message-bubble {
  max-width: 44rem;
  padding: 0.75rem 1rem;
  border-radius: 12px;
  background: var(--color-surface);
  border: 1px solid rgba(var(--color-text-muted-rgb), 0.2);
}
.message-bubble--mine {
  align-self: flex-end;
  background: var(--tint-secondary);
  border-color: var(--color-secondary-line);
}
.message-body p:last-child { margin-bottom: 0; }
.message-preview { color: var(--color-text-muted); }
/* Was 4.46:1 over --color-surface -- failing by 0.04, which is exactly the
   kind of miss that survives review by looking fine. Now 5.55 / 5.04. */
.badge--teal { background: var(--tint-secondary); color: var(--badge-teal-ink); }
/* Approved: distinct from published teal, since the whole point of the state
   is that it is signed off but not yet live. */
.badge--navy { background: var(--tint-strong); color: var(--badge-navy-ink); }

/* Matrix survey questions: statements down the side, the one shared scale
   across the top. A wide grid of radios is only readable while the column
   headings are in view, so each cell carries its own label as well and the
   two swap over at the point the table stops fitting: header row on wide
   screens, per-cell labels on narrow ones. */
.matrix-question th { text-align: left; font-weight: 600; }
.matrix-question td { text-align: center; }
.matrix-question .choice-option { display: inline-flex; align-items: center; gap: 0.35rem; }
.matrix-question__value { display: none; }

@media (max-width: 720px) {
  /* Below this the header row is wider than the screen, so the table is
     restacked one statement per block and the cells label themselves. */
  .matrix-question, .matrix-question tr, .matrix-question td, .matrix-question th {
    display: block;
    width: auto;
  }
  .matrix-question tr:first-child { display: none; }
  .matrix-question tr { margin-bottom: 1rem; }
  .matrix-question td { text-align: left; }
  .matrix-question__value { display: inline; }
}

/* Scale questions (rating and Likert): the points laid out in a row so the
   ladder is visible as a ladder. Wraps on narrow screens rather than
   scrolling, because unlike a matrix there is no header row to keep aligned. */
.scale-question { display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; }
.scale-question .choice-option { display: inline-flex; align-items: center; gap: 0.35rem; }

/* Ranking questions. The number comes from the list itself so it always
   agrees with the order on screen, and the drop indicators match reorder.js's
   so dragging feels the same here as it does when staff reorder anything. */
.ranking-question { list-style: none; margin: 0; padding: 0; counter-reset: rank; }
.ranking-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.35rem;
  background: var(--color-surface);
  border: 1px solid rgba(var(--color-text-muted-rgb), 0.25);
  border-radius: 6px;
  cursor: grab;
}
.ranking-item::before {
  counter-increment: rank;
  content: counter(rank) ".";
  font-weight: 600;
  color: var(--color-text-muted);
  min-width: 1.5rem;
}
.ranking-item__handle { margin-left: auto; color: var(--color-text-muted); }
.ranking-item--over-before { border-top: 3px solid var(--color-secondary-line); }
.ranking-item--over-after { border-bottom: 3px solid var(--color-secondary-line); }

/* Browse-mode bar: the same slim furniture the preview bar uses, tinted so
   the two are not mistaken for each other by someone who sees both. */
.preview-bar--browse { background: var(--tint-accent); border-color: var(--color-accent-line-strong); }

/* Back / Next along the authored reading order, with the course position
   between them so the counter sits where the eye already is. */
.page-flow-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--color-border);
}
.page-flow-nav__position { white-space: nowrap; }
/* A disabled control is a <span>, not a <button>: it is not focusable and
   carries no action, which is the honest markup for "there is nothing here". */
.page-flow-nav [aria-disabled="true"] { opacity: 0.45; cursor: default; }
.page-position { font-variant-numeric: tabular-nums; }

/* Stacked mode choices, so each option's consequences sit under its label
   rather than beside it. */
.choice-option--block { display: flex; gap: 0.6rem; align-items: flex-start; margin-bottom: 1rem; }
.choice-option--block span { display: block; }

/* Accordions. Built on <details>/<summary>, so the open/closed behavior is
   the browser's -- these rules only make the summary look like a control. */
.accordion-group { margin: 1.25rem 0; }
.accordion {
  border: 1px solid var(--color-border);
  border-radius: 6px;
  margin-bottom: 0.5rem;
  background: var(--color-surface);
}
.accordion__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.75rem;
  padding: 0.7rem 0.9rem;
  cursor: pointer;
  list-style: none;
}
/* The default disclosure triangle is replaced with one that rotates, so the
   marker reads the same in every browser. */
.accordion__summary::-webkit-details-marker { display: none; }
.accordion__summary::before {
  content: "\25B8";
  color: var(--color-text-muted);
  transition: transform 0.15s ease;
}
.accordion[open] > .accordion__summary::before { transform: rotate(90deg); }
.accordion__title { font-weight: 600; }
.accordion__preview { flex: 1 1 14rem; }
.accordion__content { padding: 0 0.9rem 0.9rem 1.9rem; }
.accordion__content > *:first-child { margin-top: 0; }
.accordion__actions {
  display: flex;
  gap: 0.4rem;
  padding: 0 0.9rem 0.8rem 1.9rem;
}

/* The default-scope control above a library list. Sits between the header and
   the filters, reading as a statement about what is on screen rather than as
   another filter. */
.library-scope {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1rem;
  padding: 0.6rem 0.9rem;
  border: 1px dashed var(--color-border);
  border-radius: 6px;
}

/* ---------------------------------------------------------------------------
   Organizational landing pages
   ---------------------------------------------------------------------------
   Fluid rather than fixed. The banner height is a clamp so it scales with the
   viewport instead of stepping at a breakpoint; the emblem does the same. The
   one media query below is only for the point at which a two-column split
   stops being readable -- everything above it reflows continuously.

   .container caps content at 1100px, which is right for a form and wrong for a
   landing page meant to fill a large monitor, so these pages opt out through
   .container--wide on <main>.
--------------------------------------------------------------------------- */
.container--wide { max-width: 1600px; }

.unit-hero { margin-bottom: 1.5rem; }

.unit-hero-banner {
  height: clamp(120px, 22vw, 280px);
  border-radius: var(--radius);
  background-size: cover;
  background-position: center;
  background-color: var(--color-fill-strong);
}
.unit-hero-banner--empty {
  background-image: linear-gradient(120deg, var(--color-fill-strong), var(--color-fill-strong-hover));
}

.unit-hero-body {
  display: flex;
  align-items: flex-start;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  padding: 0 clamp(0.5rem, 2vw, 1.5rem);
  flex-wrap: wrap;
}

.unit-emblem {
  width: clamp(72px, 12vw, 140px);
  height: clamp(72px, 12vw, 140px);
  border-radius: 50%;
  object-fit: cover;
  border: 4px solid var(--color-surface);
  background: var(--color-surface);
  box-shadow: var(--shadow);
  flex-shrink: 0;
  /* Only the emblem rides up over the banner. Lifting the whole row instead
     put the heading on top of the banner's bottom edge. */
  margin-top: clamp(-4rem, -7vw, -2.5rem);
}
.unit-emblem--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(1.75rem, 5vw, 3rem);
  font-weight: 700;
  color: var(--color-on-accent-inverse);
  background: var(--color-accent-fill);
}

.unit-hero-text { flex: 1 1 18rem; min-width: 0; padding-top: 0.6rem; }
.unit-hero-text h1 { margin: 0.2rem 0 0; overflow-wrap: anywhere; }
.unit-level { margin: 0; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.75rem; }
.unit-description { margin: 0.4rem 0 0; max-width: 60ch; }
.unit-breadcrumb { margin: 0; font-size: 0.85rem; display: flex; flex-wrap: wrap; gap: 0.4rem; }

.unit-layout {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2.5rem);
  align-items: start;
}
.unit-main { min-width: 0; }
.unit-side { min-width: 0; }

.announcement-feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.announcement {
  background: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: clamp(0.9rem, 2vw, 1.4rem);
}
.announcement-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.announcement-head h3 { margin: 0; overflow-wrap: anywhere; }
.announcement-body { overflow-wrap: anywhere; }
.announcement-body p:last-child { margin-bottom: 0; }

.unit-children { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.unit-children li { display: flex; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }

@media (max-width: 60rem) {
  .unit-layout { grid-template-columns: 1fr; }
}

/* Dropdown menus in the navbar. A <details> rather than JS, matching the rest
   of this project's no-framework approach; it closes on navigation because the
   page reloads.

   `.nav-menu`, NOT `.nav-units`, SINCE THERE ARE SEVERAL. The Units menu was
   the first and gave this pattern its name; Library, Authoring and Admin now
   use the same markup and the same rules. The Units <details> keeps its
   `nav-units` class as an identifying hook -- it is what tells "this viewer
   has org units" apart from "this viewer has a dropdown" -- but every style
   below is on the generic class, so the four menus cannot drift apart. */
.nav-menu { position: relative; }
.nav-menu > summary { cursor: pointer; list-style: none; padding: 0.35rem 0; }
.nav-menu > summary::-webkit-details-marker { display: none; }
.nav-menu > summary::after { content: " \25BE"; }
.nav-menu-panel {
  position: absolute;
  right: 0;
  top: 100%;
  z-index: 40;
  min-width: min(16rem, 80vw);
  max-height: min(70vh, 32rem);
  overflow-y: auto;
  background: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);
  padding: 0.6rem 0.75rem;
  display: grid;
  gap: 0.15rem;
}
/* THE LEGIBILITY FIX, AND THE COLOR THAT HAD TO BE SAID OUT LOUD.
   These links sat on a near-white panel while inheriting `.navbar-links a`'s
   #dfe7ea -- a pale blue-gray chosen for text on navy. Light gray on white,
   at roughly 1.2:1.

   NOT the dead-variable cause found in the progress bars: every property named
   here exists. This is an inherited color crossing a background change, which
   no variable audit would catch. The panel opts out of the dark-navbar palette
   because it is not on the navbar's background, so it states its own color
   rather than taking one meant for a different ground. */
.nav-menu-panel a {
  display: block;
  padding: 0.3rem 0.25rem;
  overflow-wrap: anywhere;
  color: var(--color-heading);
}
.nav-menu-panel a:hover { color: var(--color-link-hover); }
.nav-menu-heading {
  margin: 0.5rem 0 0.1rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}

@media (max-width: 48rem) {
  /* Anchored menus have nowhere to anchor on a narrow screen, so it becomes
     part of the flow instead of floating off the edge. */
  .nav-menu-panel { position: static; box-shadow: none; min-width: 0; }
}

/* The theme picker, inside one of those dropdowns. Everything structural comes
   from `.nav-menu` above; these rules only undo the two things the global form
   styles would do to a radio group in a 16rem panel.

   The fieldset loses its border and padding because the panel is already the
   box -- a second outline inside it reads as two containers. `label` is a
   block at 0.75rem top margin globally, which stacks four radios with more air
   than the panel has; the flex row puts each control beside its own words and
   the gap does the spacing.

   NO COLOR RULES AT ALL, which is the point of the whole token pass: the panel
   is --color-surface, its text is inherited, and this control was correct in
   all three themes without a line being written for any of them. */
.nav-theme .theme-choices {
  border: 0;
  padding: 0;
  margin: 0 0 0.6rem;
  min-width: 0;
}
.nav-theme .nav-menu-heading { margin-top: 0; padding: 0; }
.nav-theme .theme-choice {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0.15rem 0;
  font-weight: 400;
  cursor: pointer;
}
/* The global input rule is `width: 100%`, which would stretch a radio into a
   16rem bar. */
.nav-theme .theme-choice input { width: auto; margin: 0; }
.nav-theme form { margin: 0; }

/* ---------------------------------------------------------------------------
   Fiscal-year schedule button on a landing page
   ---------------------------------------------------------------------------
   Stylized rather than a text link, because this is usually the document the
   page is opened for. Fluid like the rest of the landing page: the row wraps
   and each button grows to fill, so one button spans the width and three sit
   side by side without a breakpoint.
--------------------------------------------------------------------------- */
/*
 * "A document living on this page -- open it."
 *
 * Named for the fiscal-year schedule, which introduced it, and now used by
 * course roster documents as well (_course_roster_documents.html). The two
 * differ only in the icon: a calendar for a schedule, a clipboard for a
 * roster. The class names were kept rather than generalized because renaming
 * them would edit a working feature for no gain -- but a third caller should
 * reuse this block too rather than growing a fourth card style.
 */
.schedule-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 1.25rem 0;
}
.schedule-button {
  flex: 1 1 18rem;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.9rem 1.1rem;
  border-radius: var(--radius);
  background: var(--color-fill-strong);
  color: var(--color-on-chrome);
  box-shadow: var(--shadow);
  border-left: 5px solid var(--color-accent-line);
  text-decoration: none;
}
.schedule-button:hover,
.schedule-button:focus {
  background: var(--color-fill-strong-hover);
  color: var(--color-on-chrome);
  text-decoration: none;
}
.schedule-button-icon { font-size: 1.6rem; line-height: 1; flex-shrink: 0; }
.schedule-button-text { display: flex; flex-direction: column; min-width: 0; }
.schedule-button-text strong { overflow-wrap: anywhere; }
.schedule-button-meta {
  font-size: 0.78rem;
  color: var(--color-accent-on-chrome);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* ---------------------------------------------------------------------------
   Back to top
   ---------------------------------------------------------------------------
   Bottom-right, in the same column as the chat launcher and never on top of
   it. `--stacked` lifts it clear when that corner is occupied -- by either the
   live launcher or the blocked pill, which are different elements with the
   same footprint (see base.html).

   z-index 39: below the launcher (40) so the two can never argue about which
   is in front, and well below the popup overlay (50) so a modal always covers
   it.

   Sized in rem and offset with the same 1.25rem the launcher uses, so the pair
   stay aligned at every viewport width without a breakpoint. The launcher does
   not change size responsively, so neither does the gap.
--------------------------------------------------------------------------- */
.back-to-top {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 39;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--color-fill-strong);
  color: var(--color-on-chrome);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--shadow-float);
  display: flex;
  align-items: center;
  justify-content: center;
}
.back-to-top:hover,
.back-to-top:focus-visible { background: var(--color-fill-strong-hover); color: var(--color-on-chrome); }

/* Cleared above the launcher: the launcher's own 1.25rem offset, plus its
   height, plus a gap.

   2.875rem is the launcher's MEASURED height (46px), not its nominal padding
   plus font-size -- the first version of this rule assumed 2.4rem from reading
   the padding and left a 2px gap, which is not a gap. Measured in the browser
   at 1.25rem/16px root. If the launcher's padding or label size changes, this
   needs re-measuring; the two are related by layout, not by a shared token. */
.back-to-top--stacked { bottom: calc(1.25rem + 2.875rem + 0.6rem); }

/* `hidden` must beat `display: flex`, the same note .popup-overlay carries. */
.back-to-top[hidden] { display: none; }

@media print { .back-to-top { display: none; } }

/* Media library thumbnails. Bounded rather than sized: a library holds
   portraits and wide banners alike, so the box is capped in both axes and the
   image fits inside whichever way round it is. */
.media-thumb {
  display: block;
  max-width: 100%;
  width: 100%;
  height: 150px;
  object-fit: contain;
  background: var(--color-media-bg);
  border-radius: var(--radius);
}
.media-thumb--small { width: 84px; height: 56px; }
.media-preview { max-width: 100%; height: auto; border-radius: var(--radius); }

/* ---------------------------------------------------------------------------
   Emblem and banner management (Developer only)
   ---------------------------------------------------------------------------
   THE DISPLAY SIZE IS GUARANTEED HERE, BECAUSE NOTHING ELSE CAN GUARANTEE IT.
   No upload is ever resized, cropped or measured on the way in -- this project
   does not build on Pillow, see core/media.py -- so a 4000x30 strip and a
   16x16 favicon both reach the page at
   their own dimensions. Every box below therefore fixes BOTH axes and uses
   object-fit, which crops the source into the box instead of letting the source
   set the box. `height: auto` or a single-axis `max-width` anywhere in this
   block would hand layout control back to whatever somebody uploaded.

   `cover` rather than `contain`, matching the landing page these previews are
   previews OF: the point is to show what the emblem will actually look like
   once cropped to a circle, not to show the whole file letterboxed. The library
   grid above makes the opposite choice on purpose -- .media-thumb uses
   `contain` because a library is for identifying files, not previewing a
   placement.
--------------------------------------------------------------------------- */
/* `.panel` caps itself at 640px, which is right for the forms it was built for
   and wrong here: this page is a contact sheet, its banner previews are wide by
   nature, and three levels of nesting eat the width from the left. So it opts
   out, the same way the landing pages opt out of .container's 1100px through
   .container--wide -- and for the same reason, which is that a reading measure
   and a picture grid want different widths. */
.unit-images-org {
  margin-bottom: 1.25rem;
  max-width: none;
}

/* Indent is the only thing that says "this sits under that". Bounded with
   clamp so three levels of nesting do not run off a narrow screen: at 320px
   the deepest row still has usable width. */
.unit-images-child {
  margin-left: clamp(0.5rem, 3vw, 2rem);
  padding-left: clamp(0.5rem, 2vw, 1rem);
  border-left: 2px solid var(--color-border);
}

.unit-images-row { padding: 0.75rem 0; }
.unit-images-heading h2 { margin: 0.1rem 0 0; font-size: 1.1rem; overflow-wrap: anywhere; }
.unit-images-heading .unit-level { margin: 0; }

.unit-images-previews {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  margin: 0.6rem 0;
}
.unit-image-slot { flex: 0 1 auto; min-width: 0; }
.unit-image-slot--wide { flex: 1 1 18rem; }
.unit-image-label {
  margin: 0 0 0.3rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}

.unit-image-preview {
  display: block;
  object-fit: cover;
  object-position: center;
  background: var(--color-media-bg);
  border: 1px solid var(--color-border);
}
/* Both axes fixed, and a circle, so the preview is the landing page's emblem
   at a smaller size rather than a differently-shaped approximation of it. */
.unit-image-preview--emblem {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  flex-shrink: 0;
}
/* Fixed height, width bounded by the column. A very wide source is cropped at
   the sides and a very tall one is cropped top and bottom; neither changes the
   height of the row. */
.unit-image-preview--banner {
  width: 100%;
  max-width: 100%;
  height: 72px;
  border-radius: var(--radius);
}
.unit-image-preview--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  color: var(--color-text-muted);
  background: repeating-linear-gradient(
    45deg, var(--color-media-bg), var(--color-media-bg) 8px, var(--color-media-bg-alt) 8px, var(--color-media-bg-alt) 16px
  );
}

.unit-images-editor { margin-top: 0.4rem; }
.unit-images-editor summary { cursor: pointer; font-size: 0.9rem; }
.unit-images-fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 0.6rem 0.9rem;
  margin: 0.6rem 0;
  min-width: 0;
}
.unit-images-fieldset legend { font-weight: 600; padding: 0 0.4rem; }
/* File inputs and selects both overflow their container at narrow widths
   unless told not to; `min-width: 0` on the fieldset is what lets this win. */
.unit-images-fieldset input[type="file"],
.unit-images-fieldset select { max-width: 100%; }

/* ---------------------------------------------------------------------------
   Landing-page blocks
   ---------------------------------------------------------------------------
   Blocks stack in one column in author order. The old page had a fixed
   two-column split (feed beside a sidebar), and that could not survive: once
   the sections are arbitrary and reorderable there is no longer a "main" and
   an "aside" to assign them to. Each section keeps its own internal layout --
   the stat row is still a grid, the child list still a list -- so what changed
   is the relationship between sections, not any section's own design.
--------------------------------------------------------------------------- */
.unit-block { margin-bottom: 1.5rem; }
.unit-block > h2 { margin: 0 0 0.6rem; }

/* Both axes fixed for the same reason every other image box in this project
   fixes them: nothing resizes an upload (see core/media.py) and the size
   guarantee has to be a display constraint. `contain`, not `cover`, because a
   page block is showing the picture rather than filling a shaped slot -- the
   opposite choice from the emblem, and deliberate. */
.unit-block-image {
  display: block;
  max-width: 100%;
  height: auto;
  max-height: 32rem;
  object-fit: contain;
  border-radius: var(--radius);
}
.unit-block-video { display: block; width: 100%; max-width: 48rem; }
.unit-block-text { max-width: 70ch; }
.unit-block-text img { max-width: 100%; height: auto; }

/* Aspect-ratio box, so a frame cannot set the page's height from whatever it
   happens to load. */
.unit-block-embed {
  position: relative;
  width: 100%;
  max-width: 60rem;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--color-media-bg);
}
.unit-block-embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.unit-page-edit-bar { justify-content: flex-end; margin-bottom: 0.5rem; }

/* The builder's own list. `.card` is what reorder.js looks for, so these keep
   that class and only add to it. */
.block-list { display: flex; flex-direction: column; gap: 0.6rem; }
.block-card { padding: 0.7rem 0.9rem; cursor: grab; }
.block-card:active { cursor: grabbing; }
.block-card-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.block-card-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-top: 0.4rem;
}
.block-card-actions form { display: inline; }


/* ---------------------------------------------------------------------------
   A control the installation's plan does not include.

   Shown rather than hidden, and it says why -- see
   core/templates/core/_plan_locked.html for the reasoning, which is that a
   feature missing because it was never bought is something a customer may want
   to buy, unlike a permission they will never hold.

   It has to read as INERT AT A GLANCE, or it is worse than hiding it: somebody
   who clicks and gets nothing has been told less than somebody who saw no
   control at all. So it drops the accent colour entirely, loses the pointer,
   and carries its reason as visible text rather than as a tooltip.

   NOT `opacity`, which would dim the reason text along with the label and push
   it under contrast minimums. `--color-text-muted` is used instead -- the
   variable this site already has for secondary text -- so both the label and
   the reason stay legible; the whole point of the reason is that it can be
   read.

   MEASURED, NOT ASSUMED. #5a6672 on the page background (#f6f1e4) is 5.13:1,
   clear of the 4.5:1 WCAG AA minimum for normal-size text, which is the bar
   that applies: the label is 16px/600 and the reason 12.5px/400, and neither
   qualifies as large text. An earlier draft of this block invented a
   `--color-muted` variable that does not exist in this stylesheet, so it fell
   back to #6b7280 and measured 4.29:1 -- a real failure, found by measuring
   rather than by looking.
   --------------------------------------------------------------------------- */
.btn.is-plan-locked {
  background: transparent;
  color: var(--color-text-muted);
  border: 1px dashed var(--color-text-muted);
  cursor: default;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  line-height: 1.3;
  text-align: left;
}

.btn.is-plan-locked:hover {
  /* No hover affordance. It does not do anything. */
  background: transparent;
  color: var(--color-text-muted);
}

.plan-locked-reason {
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0.01em;
}


/* ---------- Card lists: micro-lessons and informational videos ----------
   The grid itself is `.tile-grid`, unchanged and shared with the catalog and
   the libraries. What is new here is the thumbnail on top of a card, and the
   pagination underneath the grid.

   TWELVE CARDS A PAGE IS TIED TO THIS GRID, not chosen freely. `.tile-grid` is
   `repeat(auto-fill, minmax(260px, 1fr))` with a 1.25rem gap inside a 1052px
   content column, so four columns (1100px) do not fit and three (820px) do --
   three on a desktop, two on a tablet, one on a phone, and 12 divides evenly
   by all three. If the 260px floor or the container width ever changes,
   `cardlist.PER_PAGE` changes with it. The arithmetic is written out in that
   module. */
.card--media { padding-top: 0.75rem; }

.card-thumb {
  display: block;
  width: 100%;
  /* A FIXED RATIO RATHER THAN A FIXED HEIGHT, so a row of cards lines up
     whatever shape the uploaded pictures are. `object-fit: cover` crops
     rather than distorting: a stretched face is worse than a cropped one,
     and the picture is decorative here anyway -- see _card_list.html for why
     its alt text is empty. */
  aspect-ratio: 16 / 9;
  max-width: 100%;
  object-fit: cover;
  border-radius: calc(var(--radius) - 2px);
  margin-bottom: 0.85rem;
  background: var(--color-border);
}

.card--media h3 a { text-decoration: none; }
.card--media h3 a:hover { text-decoration: underline; }

/* ---------- Pagination ----------
   The first pagination in this application. See core/cardlist.py. */
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.4rem 0.75rem;
  margin: 2rem 0 0.75rem;
}

.pagination-numbers {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.pagination-number,
.pagination-step,
.pagination-gap {
  display: inline-block;
  /* 44px of target at the default font size. Below that a page number is a
     control that a touch user misses, which on a paginated list means
     landing on the wrong page rather than failing to act. */
  min-width: 2.75rem;
  padding: 0.55rem 0.7rem;
  text-align: center;
  border-radius: var(--radius);
  text-decoration: none;
}

.pagination-number,
.pagination-step {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
}

.pagination-number:hover,
.pagination-step:hover { border-color: var(--color-accent-line-strong); }

.pagination-number.is-current {
  border-color: var(--color-secondary-line);
  background: var(--color-secondary-fill);
  color: var(--color-on-secondary);
  font-weight: 600;
}

.pagination-step.is-disabled,
.pagination-gap {
  border-color: transparent;
  background: none;
  color: var(--color-text-muted);
}

.pagination-summary { text-align: center; }

/* ---------- One micro-lesson ---------- */
.micro-lesson-video { margin-bottom: 1.5rem; }
.micro-lesson-body { margin-bottom: 2rem; }

.micro-check {
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-secondary-line);
  border-radius: var(--radius);
  background: var(--color-surface);
  padding: 1rem 1.15rem;
  margin-bottom: 1rem;
}

.micro-check-question { font-weight: 600; margin: 0 0 0.75rem; }

.micro-check-options {
  border: 0;
  margin: 0 0 0.75rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.micro-check-option {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-weight: 400;
}

.micro-check-verdict { margin: 0.75rem 0 0; font-size: 0.92rem; }
/* NOT COLOR ALONE. Each verdict already says what it means in words -- "That
   is right" and "Not quite" -- so the color reinforces a message that is
   fully carried by the text, which is what WCAG 1.4.1 asks for. */
.micro-check-verdict.is-correct { color: var(--color-secondary); font-weight: 600; }
.micro-check-verdict.is-wrong { color: var(--color-text-muted); }

.micro-lesson-done {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--color-border);
}

.info-video-player { margin-bottom: 1.25rem; }


/* ---------- Four-month course schedule ----------
   A table per month under its own heading, not a calendar grid. See
   core/courseschedule.py for why the shape is the feature: the visual
   calendar dropped its quarter and year views because a day grid cannot
   render them, and a row per run is what that removal pointed at.

   The default container, not .container--wide: three narrow columns do not
   need 1600px. */
.course-schedule-month + .course-schedule-month { margin-top: 2.25rem; }

.course-schedule-table { width: 100%; }

/* The dates column is the one a reader scans down, so it is kept off the
   ragged left edge a proportional font gives a mixed month/number string.
   `tabular-nums` lines the day numbers up without switching to a monospace
   face, which would read as code rather than as dates. */
.course-schedule-dates {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The code is reference detail rather than something to read, so it is quieter
   than the title beside it -- the same weighting `_course_tile_title.html`
   applies for the same reason. `tabular-nums` again because a code is
   frequently digits and a column of them should align. */
.course-schedule-code {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------------
   SECURITY MARKING, FOOTER LINKS, IDLE SIGN-OUT WARNING
   Design docs/compliance-profiles-design.md §2.2.

   NOT THEMED. The marking ignores data-theme on purpose: a security marking
   does not change color because somebody picked Dark. Black on white, bold,
   capitalized and centered -- the NARA CUI Marking Handbook's "bold capitalized
   black text ... centered". No official color exists for a CUI banner (DoD CUI
   FAQ: "no requirements to make the banner line ... purple"). 21:1 contrast in
   every theme, which is the verification.

   TOP STICKY, BOTTOM IN FLOW. DoDM 5200.01 Vol 2 para 17.c accepts "linear
   text appearing on both the top and bottom of the page" for web pages. The
   top bar stays visible over a long page; a second sticky bar would take
   another strip of a 320px-wide screen (WCAG 1.4.10), so the bottom one sits
   at the end of the page.

   ABOVE EVERY OVERLAY WE OWN -- popups (50), the learner tour's scrim (10000)
   -- and BELOW THE SKIP LINK, which is raised to stay reachable over it.
   --------------------------------------------------------------------------- */
.compliance-marking {
  background: var(--brand-marking-paper);
  color: var(--brand-marking-ink);
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  line-height: 1.6;
  text-align: center;
  text-transform: uppercase;
  padding: 0.1rem 1rem;
  overflow-wrap: anywhere;
}
.compliance-marking--top {
  position: sticky;
  top: 0;
  z-index: 10001;
  border-bottom: 1px solid var(--brand-marking-ink);
}
.compliance-marking--bottom {
  border-top: 1px solid var(--brand-marking-ink);
}
.has-compliance-marking .skip-link { z-index: 10002; }
/* A popup's scrim starts below the marking rather than covering it. */
.has-compliance-marking .popup-overlay { top: 1.8rem; }

@media print {
  .compliance-marking--top { position: static; }
}

.site-footer {
  margin-top: 2rem;
  padding: 1rem;
  border-top: 1px solid var(--color-border);
  font-family: var(--font-sans);
  font-size: 0.9rem;
}
.site-footer-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem 1.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.session-timeout {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  z-index: 10003;
  max-width: min(32rem, calc(100vw - 2rem));
  padding: 1rem 1.25rem;
  background: var(--color-surface);
  color: var(--color-text);
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);
}
.session-timeout[hidden] { display: none; }
