/* ============================================================================
   Longwood 2026 — CSS foundation
   Plain CSS. No build step. Lives in the T4 Media Library as one file.

   Layer order is declared ONCE, here, and is the whole specificity strategy.
   Anything in a later layer beats an earlier layer regardless of selector
   specificity — which is why `.breadcrumb a` can safely override `a` without
   an arms race, and why editor-pasted markup can never outrank components.

   T4 CONSTRAINT: Media Library files process T4 tags, never Handlebars. Nothing
   in here may contain {{ }}. Media paths CSS needs (the rotunda watermark) are
   passed in as custom properties, set inline by the content item.

   NOT BUILT YET:
     * site footer content item  — .site-footer styles exist, markup does not
     * homepage transparent header variant
     * a real Breadcrumbs nav object (105 is Related Content and renders nothing)
   ========================================================================= */

@layer reset, tokens, base, layout, components, editorial, utilities;

/* --------------------------------------------------------------------------
   TOKENS
   -------------------------------------------------------------------------- */
@layer tokens {
  :root {
    /* ── Brand channels ──────────────────────────────────────────────────
       Space-separated, NOT comma-separated. Commas work for rgb(var(--x))
       but break rgb(var(--x) / 0.3). Space separation supports both. */
    --longwood-blue:            4 30 66;
    --longwood-blue-alt:        0 32 91;
    --longwood-gray:            158 162 162;

    --secondary-red:            202 35 45;
    --secondary-red-tert-1:     240 83 63;
    --secondary-red-tert-2:     252 187 118;

    --secondary-blue:           0 95 158;
    --secondary-blue-tert-1:    0 154 218;
    --secondary-blue-tert-2:    150 209 242;

    --secondary-teal:           0 94 99;
    --secondary-teal-tert-1:    0 176 189;
    --secondary-teal-tert-2:    166 219 214;

    --secondary-green:          73 104 38;
    --secondary-green-tert-1:   114 191 68;
    --secondary-green-tert-2:   196 216 46;

    /* ── Semantic aliases ────────────────────────────────────────────────
       Components reference THESE, never the raw channels. Rebrand later by
       repointing one line instead of hunting through every rule. */
    --brand:        rgb(var(--longwood-blue));        /* #041E42 header, footer */
    --tile:         rgb(var(--secondary-blue));       /* #005F9E social tiles */
    --accent:       rgb(var(--secondary-red-tert-2)); /* #FCBB76 links on dark */

    /* No current consumer. Kept because they are part of the palette's semantic
       layer, not because anything is waiting on them. Prune if still unused
       once the footer lands. */
    --brand-alt:    rgb(var(--longwood-blue-alt));    /* #00205B */
    --muted:        rgb(var(--longwood-gray));

    /* Lancer Dashboard. The tab is the panel's handle, so both derive from one
       teal. The tab is knocked further back toward the navy behind it. */
    --dashboard-bg:     color-mix(in oklch, rgb(var(--secondary-teal)) 88%, var(--paper));
    --dashboard-tab-bg: color-mix(in oklch, rgb(var(--secondary-teal)) 62%, var(--brand));

    --paper:        #ffffff;
    --ink:          #1c1c1c;

    /* Derived translucents. Only possible because the channels are spaced. */
    --rule-on-dark: rgb(var(--longwood-gray) / 0.4);
    --scrim:        rgb(var(--longwood-blue) / 0.6);  /* dashboard ::backdrop */

    /* Field surface. Deliberately NOT a brand colour: a translucent white
       composites over whatever sits behind it, so the same token works on the
       opaque navy header and on the homepage header floating over a hero. */
    --field-bg:       rgb(255 255 255 / 0.1);

    /* Focus INVERTS the field rather than drawing a ring. Measured:
         resting rgb(29 52 85)  ->  focused #fff   = 12.47:1
       WCAG 1.4.11 wants >= 3:1 between the two states. A darkening to brand
       navy, which is what this used to do, scores 1.33:1 and fails. */
    --field-bg-focus:  var(--paper);
    --field-ink-focus: var(--ink);

    /* Type — display carries the personality; body stays quiet */
    --font-display: "Garamond Premier", "Chronicle Display", Georgia, serif;
    --font-body:    "Gotham", system-ui, -apple-system, sans-serif;

    /* Fluid scale — no breakpoint hopping */
    --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
    --step-2: clamp(1.5rem, 1.3rem + 1vw, 2rem);
    --step-4: clamp(2.5rem, 1.8rem + 3.5vw, 4.5rem);

    /* Spacing */
    --space-xs: 0.25rem;
    --space-s: 0.75rem;
    --space-m: 1.5rem;
    --space-l: 3rem;

    --header-h: 5.5rem;

    /* Width of the menu panel's VISIBLE face, and of the header's search block.
       They must be identical so the search does not jump when the menu opens.
       The `45` percentage is repeated at each use site because cqw and % resolve
       against different boxes; only the max is shared. */
    --panel-max: 62rem;

    /* Shared by the menu panel's clip-path AND the menu bar's start padding.
       The header's search inset does NOT use it — the rake cancels out there. */
    --rake: 7rem;

    /* Lancer Dashboard tab. Lean = slant / tab height; lower slant = gentler.
       --tab-drop is the gap between the header's underside and the tab. Both
       tabs consume it, so they cannot drift apart. */
    --tab-slant: 0.4rem;
    --tab-drop:  var(--space-xs);
    --wrap: 78rem;
  }
}

/* --------------------------------------------------------------------------
   RESET
   -------------------------------------------------------------------------- */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  /* hr is included deliberately: the UA sheet gives it `margin-inline: auto`,
     which silently centres any rule you try to left-align. */
  body, h1, p, ul, ol, hr { margin: 0; padding: 0; }
  ul[class], ol[class] { list-style: none; }
  img, svg { display: block; max-width: 100%; }

  /* Keyboard focus. `outline: 0` here would be a WCAG 2.4.7 failure across the
     whole site — the hamburger, close button, both dashboard tabs and every
     editor-authored link would have NO visible focus at all. Opt out per
     component, and only where that component supplies its own indicator.

     currentColor, not --accent. A fixed peach ring scores 1.68:1 on the search
     field's inverted white surface and fails 1.4.11. currentColor is the text
     colour, which 1.4.3 already guarantees is >= 4.5:1 against its own
     background — so the ring can never be under-contrast, on any surface. */
  :focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      transition-duration: 0.01ms !important;
      /* Delays too: zeroing only duration leaves a staggered *pop*. */
      animation-delay: 0s !important;
      transition-delay: 0s !important;
    }
  }
}

/* --------------------------------------------------------------------------
   BASE
   -------------------------------------------------------------------------- */
@layer base {
  body {
    font-family: var(--font-body);
    font-size: var(--step-0);
    line-height: 1.55;
    color: var(--ink);
    background: var(--paper);
  }

  h1 { font-family: var(--font-display); font-size: var(--step-4); line-height: 1.05; }

  .skip-link {
    position: absolute;
    inset-inline-start: var(--space-s);
    inset-block-start: -100%;
    z-index: 100;
    padding: var(--space-s) var(--space-m);
    background: var(--paper);
    transition: inset-block-start 150ms ease;
  }
  .skip-link:focus { inset-block-start: var(--space-s); }
}

/* --------------------------------------------------------------------------
   LAYOUT
   -------------------------------------------------------------------------- */
@layer layout {
  .wrap {
    max-inline-size: var(--wrap);
    margin-inline: auto;
    padding-inline: var(--space-m);
  }

  /* Replaces Foundation's .row / .columns entirely. */
  .grid {
    display: grid;
    gap: var(--space-m);
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  }
}

/* --------------------------------------------------------------------------
   COMPONENTS
   -------------------------------------------------------------------------- */
@layer components {

  /* ── The header bar ───────────────────────────────────────────────────
     Full-bleed zones, not a padded row: logo on brand navy, search on a
     translucent-white block, hamburger on a blue block laid over its right end.
     Cells stretch to full header height, so padding lives on the CELLS. */
  .bar {
    display: grid;
    /* Two columns, not three: the hamburger OVERLAPS the search block rather
       than following it. If it took its own column, `justify-self:end` would
       pin the search block one hamburger-width left of the viewport edge —
       while the menu panel measures from the viewport edge. The search would
       jump sideways every time the menu opened. */
    grid-template-columns: auto 1fr;
    align-items: stretch;
    gap: 0;
    padding: 0;
    color: var(--paper);
    container-type: inline-size;
  }

  /* relative: the dashboard tab hangs off the bottom-right of this box */
  .site-header {
    background: var(--brand);
    position: relative;
    min-block-size: var(--header-h);
  }

  .bar__logo {
    display: grid;
    place-items: center;
    padding-inline: var(--space-m);
  }
  .bar__logo img { inline-size: auto; block-size: 3rem; }

  /* Search block, pinned to the viewport's right edge and running UNDER the
     hamburger. Brand navy fills the gap between it and the logo.
     grid-column is set here, never on .bar__search globally — it would leak
     into the menu panel's copy, which needs column 1. */
  .site-header .bar__search {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    /* cqw = % of the header (.bar sets container-type). Matches the panel's %
       of the popover, since both boxes are the viewport width. */
    inline-size: min(45cqw, var(--panel-max));

    /* The magnifier must not move when the menu opens:
         panelFace  = viewportRight - panelWidth
       Both fields sit flush on panelFace and both get their start inset from
       the shared .bar__search rule, so
         header input == menu input == face + space-m
       identically, and neither depends on --rake. Only the END inset differs:
       it clears the overlapping square hamburger (--header-h). */
    padding-inline-end: var(--header-h);
  }
  /* No rule under the header's INPUT — the field block carries the brand rule
     on focus instead. The menu's copy keeps its resting underline, as on live. */
  .site-header .bar__search form { border-block-end: 0; }


  /* ── Field surface. Shared by the header's search and the menu panel's. ──
     Geometry differs (see each); the surface, the focus behaviour and the
     inset from the panel face are identical, so the two read as one control
     that simply changes what sits behind it. */
  .bar__search {
    display: grid;
    align-items: center;
    padding-inline: var(--space-m);
    background: var(--field-bg);

    /* Reserved at rest, coloured on focus. Declaring the border only in the
       focus state would grow the box by 1px and nudge everything up. */
    border-block-end: 1px solid transparent;

    transition: background-color 150ms ease,
                color 150ms ease,
                border-color 150ms ease;
  }

  /* Focus INVERTS the field: a 12.47:1 change of state, clearing WCAG 1.4.11's
     3:1 bar with room to spare, and making the query far more legible than
     white-on-navy while it is being typed. The brand rule underneath reinforces
     it (16.5:1 on white).
     `color` drives the input text AND the magnifier, which is currentColor. */
  .bar__search:focus-within {
    background: var(--field-bg-focus);
    color: var(--field-ink-focus);
    border-block-end-color: var(--brand);
  }
  .bar__search:focus-within input::placeholder {
    color: rgb(0 0 0 / 0.6);   /* 5.74:1 on white */
  }
  /* The underline would read as a stray grey rule on the inverted field. */
  .bar__search:focus-within form { border-block-end-color: transparent; }

  .bar__search form {
    display: flex;
    align-items: center;
    gap: var(--space-s);
    inline-size: 100%;
    border-block-end: 1px solid var(--rule-on-dark);
    padding-block-end: 0.3rem;
    transition: border-color 150ms ease;
  }
  .bar__search input {
    flex: 1;
    background: transparent;
    border: 0;
    color: inherit;
    font: inherit;
    padding: 0.2rem 0;
    min-inline-size: 0;
  }
  .bar__search input::placeholder { color: rgb(255 255 255 / 0.75); }
  .bar__search input { caret-color: var(--brand); }

  /* The native ::-webkit-search-cancel-button is suppressed. It exists only in
     Chrome/Safari, is not keyboard focusable, and its hit area is far under the
     24x24 minimum. .bar__clear below replaces it on every browser. */
  .bar__search input[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
  }

  /* Visible only when the field has content. :placeholder-shown is true for an
     empty input, so :not(:placeholder-shown) means "the user typed something".
     Requires the input to carry a placeholder — it does. */
  .bar__clear {
    display: grid;
    place-items: center;
    align-self: center;
    inline-size: 1.75rem;   /* >= 24px hit target, WCAG 2.5.8 */
    aspect-ratio: 1;
    flex: none;
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;
    padding: 0;
    opacity: 0;
    visibility: hidden;
    transition: opacity 120ms ease;
  }
  .bar__search:has(input:not(:placeholder-shown)) .bar__clear {
    opacity: 0.65;
    visibility: visible;
  }
  .bar__clear:hover,
  .bar__clear:focus-visible { opacity: 1; }

  /* The ONLY place the ring is suppressed on a field, and only because the
     inversion above is a stronger indicator. Do not widen this selector. */
  .bar__search input:focus-visible { outline: 0; }

  /* Real <button>s, never <a href="#">. Keyboard and AT for free. */
  .bar__menu-toggle,
  .bar__close {
    display: grid;
    place-items: center;
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;
    padding: var(--space-s);
  }

  /* Square blue block, full header height, laid OVER the right end of the
     search block (same grid cell). */
  .site-header .bar__menu-toggle {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    background: color-mix(in oklch, rgb(var(--secondary-blue)) 72%, var(--brand));
    padding-inline: var(--space-m);
    aspect-ratio: 1;
  }
  .site-header .bar__menu-toggle:hover {
    background: rgb(var(--secondary-blue));
  }

  /* Angled tab tucked under the header's right edge. Absolute, so it does not
     occupy a header grid column. The clip rakes the left edge the same way the
     menu panel does. */
  .dashboard-toggle {
    position: absolute;
    inset-inline-end: 0;

    background: var(--dashboard-tab-bg);
    color: var(--paper);
    border: 0;
    cursor: pointer;

    font: inherit;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    /* line-height 1, not 0.6. Sub-1 clips descenders and makes the tab's height
       — and therefore its lean angle — hostage to font metrics. Control height
       with padding-block instead. 0.99rem preserves the 44.5px it had at 0.6. */
    line-height: 1;

    /* Left padding derives from the slant, so text never crowds the angle. */
    padding-block: 0.99rem;
    padding-inline: calc(var(--tab-slant) + var(--space-s)) var(--space-s);
    clip-path: polygon(var(--tab-slant) 0, 100% 0, 100% 100%, 0 100%);

    /* `100% + drop`, not margin-top. Physical margin in a logical file, and on
       an absolutely positioned box it fights the in-menu copy's offset. */
    inset-block-start: calc(100% + var(--tab-drop));
  }
  .dashboard-toggle:hover { background: var(--dashboard-bg); }

  /* The header's tab sits beneath the popover's top layer and cannot be raised.
     This copy lives inside the panel and lands in the same place.

     The header tab is `100% + drop` of the header's ACTUAL height; this one is
     `--header-h + drop`. They coincide only while the header never outgrows its
     min-block-size. Sharing --tab-drop at least keeps the gap in step. */
  .dashboard-toggle--in-menu {
    position: absolute;
    inset-block-start: calc(var(--header-h) + var(--tab-drop));
    inset-inline-end: 0;
  }

  /* Container query, not a media query: the bar restyles on its OWN width, so
     it behaves identically wherever it is placed. .bar sets container-type. */
  @container (width < 48rem) {
    .site-header .bar__search { display: none; }
    .dashboard-toggle {
      font-size: 0.65rem;
      padding-block: 0.85rem;
      /* Shrink the slant WITH the tab, or the lean steepens as it shortens.
         0.55rem was calibrated against an old 0.75rem base and is now larger
         than the base — it made the narrow tab steeper. 0.34rem holds ~8.2deg,
         matching the wide tab. */
      --tab-slant: 0.34rem;
    }
  }

  /* When the menu is open its panel covers the header's right side — but the
     raked corner leaves a wedge where the header's search icon peeks through.
     Hide the controls rather than chase the geometry. The logo stays: it shows
     through the transparent region by design.

     visibility, not display: no layout shift when the menu closes. */
  body:has(.main-menu:popover-open) .site-header .bar__search,
  body:has(.main-menu:popover-open) .site-header .bar__menu-toggle {
    visibility: hidden;
  }

  /* ── Main menu ─────────────────────────────────────────────────────────
     A navy panel raked along its left edge, pinned right. Everything left of
     the rake is TRANSPARENT — the page's hero and logo show through from
     beneath. There is no image inside this component.

     Do NOT put `display` on the base rule. The UA sheet closes a popover with
     [popover]:not(:popover-open) { display: none } — an author `display: grid`
     here beats it, leaving an invisible full-screen layer eating every click.
     Declare display ONLY in the open state; allow-discrete animates it. */
  [popover].main-menu {
    position: fixed;
    inset: 0;
    inline-size: 100%;
    max-inline-size: 100%;
    block-size: 100dvh;
    max-block-size: 100dvh;
    margin: 0;
    border: 0;
    padding: 0;
    overflow: clip;
    background: transparent;
    color: var(--paper);

    opacity: 0;
    transition: opacity 250ms ease, overlay 250ms allow-discrete,
                display 250ms allow-discrete;
  }
  [popover].main-menu:popover-open { display: block; opacity: 1; }
  @starting-style {
    [popover].main-menu:popover-open { opacity: 0; }
  }
  /* Live site has no scrim: the hero stays at full brightness. */
  [popover].main-menu::backdrop { background: transparent; }

  .main-menu__panel {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;

    /* +rake because the clip eats that much off the top-left corner. The
       VISIBLE face is therefore min(45%, --panel-max) — identical to the
       header's search block, which is what keeps the search from moving. */
    inline-size: calc(min(45%, var(--panel-max)) + var(--rake));

    background: var(--brand);
    /* Left edge leans inward as it descends: top-left is rake px to the right */
    clip-path: polygon(var(--rake) 0, 100% 0, 100% 100%, 0 100%);

    display: grid;
    grid-template-rows: auto 1fr;
    isolation: isolate;
    overflow-y: auto;
  }

  /* Rotunda watermark. --watermark is set inline on the element, because
     Media Library paths come from a T4 tag, and T4 tags do not run in CSS. */
  .main-menu__panel::after {
    content: "";
    position: absolute;
    inset-block-end: 0;
    inset-inline-end: 0;
    inline-size: min(60%, 34rem);
    aspect-ratio: 1;
    background-image: var(--watermark, none);
    background-size: contain;
    background-position: bottom right;
    background-repeat: no-repeat;
    opacity: 0.12;
    z-index: -1;
    pointer-events: none;
  }

  .main-menu__bar {
    display: grid;
    grid-template-columns: 1fr auto;
    /* stretch, not center: the field block fills the bar's height, as it does
       in the header. */
    align-items: stretch;
    gap: var(--space-m);
    min-block-size: var(--header-h);
    /* Start padding is exactly one rake, no more: the clip cuts furthest right
       at the panel's TOP, which is where this bar sits, so the field's left
       edge lands flush on the panel face — mirroring the header.

       The remaining space-m before the magnifier comes from .bar__search's own
       padding, which is how header input == menu input == face + space-m. */
    padding: var(--space-s) var(--space-m) var(--space-s) var(--rake);
  }
  .main-menu__bar .bar__search { grid-column: 1; }
  .main-menu__bar .bar__close  { grid-column: 2; }

  /* Start padding clears one full rake: the clip cuts furthest right at the
     panel's TOP, so measuring against the midpoint would let the first nav line
     collide with the diagonal.

     fit-content shrink-wraps the widest child — "Admissions", or the
     Visit/Apply/Give row if that runs longer. .main-menu__rule then takes 100%
     and ends exactly where the longest line ends. No magic number. */
  .main-menu__nav {
    align-self: center;
    inline-size: fit-content;
    padding-inline: calc(var(--rake) + var(--space-m) * 1.6) var(--space-l);
    padding-block-end: var(--space-l);
  }

  .main-menu__primary a {
    display: inline-block;
    font-family: var(--font-display);
    font-size: clamp(2rem, 1rem + 3vw, 3.75rem);
    line-height: 1.35;
    color: var(--paper);
    text-decoration: none;
    transition: color 150ms ease;
  }
  .main-menu__primary a:hover,
  .main-menu__primary a:focus-visible { color: var(--accent); }
  /* Supplies its own indicator (colour + the entrance offset), so the ring
     would be redundant. This is the ONLY sanctioned opt-out pattern. */
  .main-menu__primary a:focus-visible { outline: 0; }

  /* ── Staggered entrance ────────────────────────────────────────────────
     Links rise in from the right, transparent to opaque, one after another.
     No JS: @starting-style gives the browser a "before" state to transition
     FROM at the moment the popover enters the top layer.

     --i is the stagger index, set on the <li> and inherited by the <a>. */
  .main-menu__primary a,
  .main-menu__actions a {
    opacity: 0;
    translate: 1.25rem 0;
    transition: opacity 320ms ease,
                translate 320ms cubic-bezier(0.22, 1, 0.36, 1),
                color 150ms ease;
  }

  [popover].main-menu:popover-open .main-menu__primary a,
  [popover].main-menu:popover-open .main-menu__actions a {
    opacity: 1;
    translate: 0 0;
    /* Delay lives on the OPEN state only, so closing snaps back together
       instead of unravelling in reverse. */
    transition-delay: calc(90ms + var(--i, 0) * 55ms);
  }

  @starting-style {
    [popover].main-menu:popover-open .main-menu__primary a,
    [popover].main-menu:popover-open .main-menu__actions a {
      opacity: 0;
      translate: 1.25rem 0;
    }
  }

  .main-menu__primary li:nth-child(1) { --i: 0; }
  .main-menu__primary li:nth-child(2) { --i: 1; }
  .main-menu__primary li:nth-child(3) { --i: 2; }
  .main-menu__primary li:nth-child(4) { --i: 3; }

  .main-menu__actions li:nth-child(1) { --i: 5; }
  .main-menu__actions li:nth-child(2) { --i: 6; }
  .main-menu__actions li:nth-child(3) { --i: 7; }

  /* The UA stylesheet sets `hr { margin-inline: auto }`, which centres the
     rule instead of aligning it with the nav text. Zero the start margin.
     inline-size:100% of a fit-content parent == the width of the longest line. */
  .main-menu__rule {
    border: 0;
    border-block-start: 1px solid var(--rule-on-dark);
    margin-block: var(--space-m);
    margin-inline: 0;
    inline-size: 100%;
  }

  .main-menu__actions {
    display: flex;
    gap: var(--space-l);
  }
  .main-menu__actions a {
    /* inline-block, not inline: `translate` has no effect on non-replaced
       inline boxes, so the entrance animation would silently do nothing. */
    display: inline-block;
    font-family: var(--font-display);
    font-size: var(--step-2);
    color: var(--accent);
    text-decoration: none;
  }
  .main-menu__actions a:hover { text-decoration: underline; }

  /* Narrow: full-width panel, no rake. Zeroing --rake cascades into the panel's
     clip-path and both padding calcs at once. Viewport query is correct here:
     the popover always fills the viewport, so there is no container to query. */
  @media (width < 55rem) {
    .main-menu__panel {
      --rake: 0rem;
      inline-size: 100%;
      clip-path: none;
    }
    .main-menu__actions { gap: var(--space-m); }
  }

  /* ────────────────────────────────────────────────────────────────────────
     SITE ALERTS
     Slim bar at the top of the page, one per active feed item. Most page loads
     render none: the <section id="alerts"> ships empty and stays empty.

     Colours live HERE, not in JavaScript. The old script set them inline
     (`$article.css('background-color', cfg.bgColor)`), which beats every
     cascade layer — the same trap as pasted TinyMCE styles.

     The old emergency red was #df2833, which is not in the brand palette.
     Brand red is #CA232D and it contrasts BETTER: 5.54:1 against white, versus
     4.68:1. Fixing the colour improved the accessibility.
     ──────────────────────────────────────────────────────────────────────── */
  #alerts:empty { display: none; }   /* no wrapper on the 99% of pages with none */

  .alert-item {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-s);
    padding: var(--space-s) var(--space-m);
    font-size: 0.95rem;

    /* Collapse on dismiss. interpolate-size lets block-size animate from `auto`
       to 0 with no measuring, no inline styles, no jQuery slideUp().
       Chromium-only today; elsewhere the height snaps and only the opacity
       animates, which is a perfectly acceptable degradation. */
    interpolate-size: allow-keywords;
    transition: opacity 200ms ease, block-size 200ms ease, padding 200ms ease;
  }
  .alert-item.is-dismissing {
    opacity: 0;
    block-size: 0;
    padding-block: 0;
    overflow: hidden;
  }

  .alert-item p { margin: 0; }

  .alert-item__icon {
    inline-size: 1.35rem;
    block-size: auto;
    flex: none;
  }
  /* The icon carries no fill attribute; each level supplies one. */
  .alert-item__icon path { fill: var(--alert-icon); }

  .alert-item__body {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-s);
  }

  .alert-item__details {
    color: inherit;
    font-weight: 600;
    text-underline-offset: 0.2em;
  }

  .alert-item__close {
    display: grid;
    place-items: center;
    inline-size: 1.75rem;   /* >= 24px target, WCAG 2.5.8 */
    aspect-ratio: 1;
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;
    padding: 0;
    opacity: 0.75;
  }
  .alert-item__close:hover,
  .alert-item__close:focus-visible { opacity: 1; }

  /* ── Levels. Every value is a brand token. ─────────────────────────────── */
  .alert-item--emergency {
    background: rgb(var(--secondary-red));        /* #CA232D  5.54:1 on white */
    color: var(--paper);
    --alert-icon: var(--accent);                  /* 3.30:1 on the bar */
  }
  .alert-item--alert {
    background: var(--accent);                    /* #FCBB76  10.15:1 with ink */
    color: var(--ink);
    --alert-icon: rgb(var(--secondary-red));
  }
  .alert-item--notice {
    background: var(--tile);                      /* #005F9E  6.71:1 on white */
    color: var(--paper);
    --alert-icon: rgb(var(--secondary-blue-tert-2));
  }


  /* ────────────────────────────────────────────────────────────────────────
     MAIN CONTENT REGION
     <main class="full-width"> — a real <main>, not the old <div role="main">.
     T4 injects section content between the page layout's Head and Footer code.
     ──────────────────────────────────────────────────────────────────────── */
  .full-width {
    display: block;
    min-block-size: 50vh;   /* keeps the footer down on near-empty pages */
  }


  /* ────────────────────────────────────────────────────────────────────────
     PAGE HEADER + BREADCRUMB
     Old nav objects 103 (Section Title), 329 (Above Breadcrumb), 105 (Section
     Breadcrumb). 105 is a Related Content object reading a layout off the
     hidden SectionMetaDescription item, so it renders NOTHING in new sections.
     Replace it with a real Breadcrumbs navigation object.
     ──────────────────────────────────────────────────────────────────────── */
  .page-header {
    padding-block: var(--space-l);
  }

  /* Now inside <nav aria-label="Breadcrumb">, which the old markup lacked, and an
     <ol> rather than a <ul> — a breadcrumb is ordered.

     The separator is generated content, so it stays out of the accessibility
     tree. T4's Breadcrumbs nav object emits no list markup of its own: the <li>
     boundaries come from its Separator HTML (`</li><li>`). */
  .breadcrumb__list {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-s);
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: 0.9rem;
  }
  .breadcrumb__list li { display: flex; align-items: baseline; gap: var(--space-s); }

  /* Generated, so it never reaches the accessibility tree. Live uses a hyphen. */
  .breadcrumb__list li + li::before {
    content: "-";
    opacity: 0.5;
  }

  /* The last crumb is plain text, not a link — "Output the current section level
     as a link" is unchecked on the nav object. That is the whole accessibility
     fix, and T4 hands it to us as a checkbox. */
  .breadcrumb__list li:last-child { font-weight: 600; }

  .breadcrumb a { color: inherit; text-decoration: none; }
  .breadcrumb a:hover,
  .breadcrumb a:focus-visible { text-decoration: underline; }


  /* ────────────────────────────────────────────────────────────────────────
     FOOTER
     Content type "2026 Footer" (3285), in _config/footer. Unlike the header and
     dashboard this is NOT Code Only: its six elements are TinyMCE fields, so
     editors get real section links and media picking. The layout renders them
     with modifiers="medialibrary,nav_sections", which is what resolves media
     refs and section links inside authored HTML.

     Selectors hang off the LAYOUT's wrappers (.site-footer__social ul), never
     off the class names inside the authored HTML (.social-links, .last-links).
     Those live in TinyMCE and an editor can change them. The wrappers are ours.
     ──────────────────────────────────────────────────────────────────────── */
  .site-footer {
    background: var(--brand);
    color: var(--paper);
    text-align: center;
  }
  .site-footer a { color: var(--accent); }

  /* ── Social row ─────────────────────────────────────────────────────────
     The <img>s arrive carrying style="width: -1px; height: -1px" because those
     media items have no dimensions recorded. Invalid CSS, so browsers discard
     it and these rules win — but fix it at the source, since inline styles
     outrank everything here, and no width/height attrs means layout shift. */
  .site-footer__social {
    display: flex;
    justify-content: center;
    padding-block: var(--space-l);
  }
  .site-footer__social ul {
    display: flex;
    justify-content: center;
    gap: 2px;
    list-style: none;
    padding: 0;
    margin: 0;
  }
  .site-footer__social a {
    display: grid;
    place-items: center;
    inline-size: 3.5rem;
    aspect-ratio: 1;
    background: var(--tile);
    transition: background-color 150ms ease;
  }
  .site-footer__social a:hover,
  .site-footer__social a:focus-visible {
    background: rgb(var(--secondary-blue-tert-1));
  }
  .site-footer__social img {
    inline-size: 1.6rem;
    block-size: 1.6rem;
  }

  /* ── Main block ─────────────────────────────────────────────────────────── */
  .site-footer__main {
    display: grid;
    gap: var(--space-l);
    padding-block-end: var(--space-l);
  }

  /* Two columns meeting in the middle: display serif ranged right, utility
     links ranged left. Together they read as one centred block. */
  .site-footer__links {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-l);
    align-items: start;
    max-inline-size: 32rem;
    margin-inline: auto;
  }
  .site-footer__links ul {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  .site-footer__campus { text-align: end; }
  .site-footer__other  { text-align: start; }

  .site-footer__campus a {
    font-family: var(--font-display);
    font-size: clamp(2rem, 1.4rem + 2vw, 3rem);
    line-height: 1.35;
    color: var(--paper);
    text-decoration: none;
  }
  .site-footer__other a {
    font-size: 1.05rem;
    text-decoration: none;
    line-height: 2.2;
  }

  /* Colour alone cannot distinguish a link (1.4.1). Underline on interaction:
     the resting state stays clean, the affordance appears on hover and focus. */
  .site-footer__campus a:hover,
  .site-footer__campus a:focus-visible,
  .site-footer__other a:hover,
  .site-footer__other a:focus-visible,
  .site-footer__location a:hover,
  .site-footer__location a:focus-visible,
  .site-footer__legal a:hover,
  .site-footer__legal a:focus-visible {
    text-decoration: underline;
  }
  .site-footer__campus a:hover { color: var(--accent); }

  .site-footer__logo img {
    inline-size: min(26rem, 80vw);
    margin-inline: auto;
  }

  .site-footer__location { font-size: 0.95rem; }
  .site-footer__location p { margin: 0; }

  /* ── Legal strip ────────────────────────────────────────────────────────
     rgb(0 0 0 / 0.25) rather than a mixed hex: it composites over whatever is
     behind it, so it survives a rebrand of --brand untouched.
     Pale blue on the resulting band measures 10.88:1. */
  .site-footer__legal {
    background: rgb(0 0 0 / 0.25);
    padding-block: var(--space-l);
  }
  .site-footer__legal ul {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    gap: var(--space-s);
  }
  .site-footer__legal a {
    color: rgb(var(--secondary-blue-tert-2));
    font-weight: 700;
    text-decoration: none;
  }

  @media (width < 45rem) {
    .site-footer__links {
      grid-template-columns: 1fr;
      text-align: center;
    }
    .site-footer__campus,
    .site-footer__other { text-align: center; }
  }


  /* ────────────────────────────────────────────────────────────────────────
     LANCER DASHBOARD
     Slide-in panel, right edge. Replaces the Foundation off-canvas aside and
     nav object 263. Native popover: Esc, light-dismiss, focus containment and
     the top layer, all free. Zero JavaScript.

     Same rule as the main menu: NEVER declare `display` on the base selector.
     The UA sheet closes a popover with display:none and an author `display`
     beats it, leaving an invisible full-screen layer eating clicks.
     ──────────────────────────────────────────────────────────────────────── */
  [popover].dashboard {
    position: fixed;
    inset: 0 0 0 auto;          /* pinned right */
    inline-size: min(30rem, 92vw);

    /* Both of these are REQUIRED, and yes, it looks redundant next to
       inset-block: 0. It is not. `.dashboard__inner { block-size: 100% }` needs
       a definite height to resolve against, and inset alone did not give it one
       here. Removing either line silently kills the panel's scrolling.
       Tried and reverted once. Do not tidy this. */
    block-size: 100dvh;
    max-block-size: 100dvh;

    margin: 0;
    border: 0;
    padding: 0;

    background: var(--dashboard-bg);
    color: var(--paper);
    overflow: hidden;           /* the inner div scrolls, not this */

    translate: 100% 0;
    transition: translate 280ms cubic-bezier(0.22, 1, 0.36, 1),
                overlay 280ms allow-discrete,
                display 280ms allow-discrete;
  }
  [popover].dashboard:popover-open {
    display: block;
    translate: 0 0;
  }
  @starting-style {
    [popover].dashboard:popover-open { translate: 100% 0; }
  }

  /* A [popover] does not lock body scroll the way <dialog>'s modal mode does.
     Without this the wheel moves the page behind, and on a short page nothing
     moves at all — the panel looks broken. scrollbar-gutter on :root keeps the
     layout from jumping as the page's scrollbar disappears.

     Applies to the main menu too: it is full-height and has nothing to scroll. */
  :root { scrollbar-gutter: stable; }
  body:has(.dashboard:popover-open),
  body:has(.main-menu:popover-open) { overflow: hidden; }

  /* Unlike the main menu, this one DOES dim the page: the panel is narrow and
     the content behind it stays legible enough to compete for attention. */
  [popover].dashboard::backdrop {
    background: var(--scrim);
    opacity: 0;
    transition: opacity 280ms ease,
                overlay 280ms allow-discrete,
                display 280ms allow-discrete;
  }
  [popover].dashboard:popover-open::backdrop { opacity: 1; }
  @starting-style {
    [popover].dashboard:popover-open::backdrop { opacity: 0; }
  }

  /* The scroll container. Keeping it inside means the close handle stays pinned
     to the panel's corner instead of scrolling away with the content.
     block-size: 100% resolves against the panel's explicit 100dvh — see above. */
  .dashboard__inner {
    block-size: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;   /* don't chain scroll to the page behind */
    padding: var(--space-l) var(--space-m) var(--space-l) var(--space-l);
  }

  /* Was <a href="#"><i class="fa fa-angle-left"> — an anchor pretending to be a
     button. The icon font is bundled in 2023styles-1.css and does render; it is
     simply not worth a font file for two glyphs. Inline SVG instead. */
  .dashboard__close {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    z-index: 1;

    display: grid;
    place-items: center;
    inline-size: 2.75rem;
    aspect-ratio: 1;

    background: var(--dashboard-tab-bg);
    border: 0;
    color: inherit;
    cursor: pointer;
    padding: 0;
  }
  .dashboard__close:hover { background: var(--brand); }

  .dashboard__title {
    font-family: var(--font-body);
    font-weight: 700;
    font-size: var(--step-2);
    text-transform: uppercase;
    letter-spacing: 0.01em;
    line-height: 1.1;
    margin-block-end: var(--space-s);
  }

  /* Computed client-side, because this channel publishes .html and can no
     longer render PHP. Ships empty; this collapses the line entirely rather
     than leaving a dangling "Today is" if the script is blocked. */
  .dashboard__date { font-weight: 600; }
  .dashboard__date:has(time:empty) { display: none; }

  /* Spacing lives on the group, not on the date — so it survives the date
     being hidden. */
  .dashboard__group:first-of-type { margin-block-start: var(--space-l); }
  .dashboard__group + .dashboard__group { margin-block-start: var(--space-l); }

  .dashboard__heading {
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 1.35rem;
    margin-block-end: var(--space-s);
  }

  .dashboard__links {
    list-style: none;
    padding: 0;
    margin: 0 0 var(--space-m);
    display: grid;
    gap: 0.35rem;
  }
  .dashboard__links a {
    color: inherit;
    text-decoration: none;
  }
  .dashboard__links a:hover,
  .dashboard__links a:focus-visible { text-decoration: underline; }

  /* Translucent white, so it sits on the teal without a second colour token. */
  .arrow-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    padding: 0.5rem var(--space-s);
    margin-block-start: 0.35rem;
    background: rgb(255 255 255 / 0.1);
    color: inherit;
    text-decoration: none;
    transition: background-color 150ms ease;
  }
  .arrow-link + .arrow-link { margin-block-start: 0.25rem; }
  .arrow-link::after {
    content: "\2192";          /* generated: decorative, and never selectable */
    transition: translate 150ms ease;
  }
  .arrow-link:hover { background: rgb(255 255 255 / 0.2); }
  .arrow-link:hover::after { translate: 0.25rem 0; }
}

/* --------------------------------------------------------------------------
   EDITORIAL — everything TinyMCE emits.
   Scoped so author-pasted markup can never leak into component styling.
   Sits BELOW utilities, ABOVE nothing else that matters.
   -------------------------------------------------------------------------- */
@layer editorial {
  @scope (.prose) {
    :scope { max-inline-size: 68ch; }
    p + p { margin-block-start: 1em; }
    /* Old footer shipped <p><p>…</p><br></p>. Defensive, but harmless. */
    p:empty, br:only-child { display: none; }
  }
}

/* --------------------------------------------------------------------------
   UTILITIES — the only place !important is ever acceptable, and it isn't
   needed, because this layer already wins.
   -------------------------------------------------------------------------- */
@layer utilities {
  .visually-hidden:not(:focus):not(:active) {
    clip-path: inset(50%);
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    white-space: nowrap;
  }
}