/* @layer layout — structural rules, grid, spacing */

@layer layout {

  /* ─────────────────────────────────────────────
     CONTAINER
     Single reusable container element.
     Centered, with responsive inline padding.
     ───────────────────────────────────────────── */

  /* Anchor scroll offset: accounts for fixed nav so target sections
     are not hidden underneath the nav bar when navigating by anchor link. */
  html {
    scroll-padding-block-start: var(--nav-height);
  }

  /* ─────────────────────────────────────────────
     FILM GRAIN (Phase 24b)
     A document-level texture so the flat #0D0A1A reads as an aged surface
     rather than as a digital fill. This is the single cheapest change on
     the site's atmosphere-per-byte curve.

     A pseudo-element, not a markup element: it is purely presentational and
     carries no meaning, so by the separation-of-concerns rule it belongs in
     the presentation layer, not the document layer.

     Inline SVG feTurbulence as a data URI -- no HTTP request, under 300
     bytes, and permitted by the CSP's existing `img-src 'self' data:`.
     No CSP change is required for this.

     mix-blend-mode: overlay, not normal. At normal blend the same texture
     lifted #0D0A1A visibly toward grey during local verification; overlay
     modulates the colour already there instead of laying grey over it.

     z-index sits at --z-overlay (200), above content and nav but below
     --z-modal. A native <dialog> renders in the browser's top layer and is
     unaffected by z-index entirely, so dialogs are never grained over.
     ───────────────────────────────────────────── */
  body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    pointer-events: none;
    opacity: 0.045;
    mix-blend-mode: overlay;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
    background-size: 180px 180px;
  }

  /* Texture is a screen affordance. It costs ink and reads as noise on paper. */
  @media print {
    body::after {
      display: none;
    }
  }

  .container {
    inline-size: min(var(--container-max), 100%);
    margin-inline: auto;
    padding-inline: var(--space-4);
  }

  .container--narrow {
    inline-size: min(var(--container-narrow), 100%);
  }

  .container--wide {
    inline-size: min(var(--container-wide), 100%);
  }

  /* ─────────────────────────────────────────────
     NAVIGATION LAYOUT
     Flex row: brand left, links right.
     Mobile: links hidden until toggle opens them.
     ───────────────────────────────────────────── */

  .nav {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: var(--nav-height);
    z-index: var(--z-nav);
    display: flex;
    align-items: center;
  }

  .nav__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    inline-size: 100%;
  }

  /* Mobile: vertical panel, hidden by default */
  .nav__links {
    display: none;
    position: fixed;
    inset-block-start: var(--nav-height);
    inset-inline: 0;
    flex-direction: column;
    padding-block: var(--space-4);
    padding-inline: var(--space-4);
    gap: var(--space-3);
  }

  /* CSS handles visibility; JS only toggles aria-expanded */
  .nav__toggle[aria-expanded="true"] + .nav__links,
  .nav__toggle[aria-expanded="true"] ~ .nav__links {
    display: flex;
  }

  /* Desktop: horizontal inline nav.
     Breakpoint widened again, Session 30: 64rem was tuned against the
     public 6-link set (Doctrine/The Ninety/Disciplines/The Sage/Begin/
     Shop). The admin-state row (+Dashboard/Admin/Sign Out, 9 links total)
     didn't fit at that width -- each flex item shrank below its own
     content width and individual link/wordmark TEXT wrapped mid-word
     (author-reported: "SAGE"/"TRAINING" splitting onto two lines). Widened
     to 78rem so the horizontal row only activates once there's genuinely
     enough width for the fuller state; below it, the already-working
     hamburger panel handles any link count with no wrapping risk. This
     number is a reasoned adjustment (more links needing more room), not a
     pixel-measured one -- confirm at the actual admin-logged-in state
     across a couple of window widths before considering this fully closed.
     Must move together with the matching 64rem->78rem changes below
     (.nav__toggle) and in components.css (.nav__links mobile-panel query)
     -- see that file's comment for why a mismatch between any of these
     three would be worse than the original bug. */
  @media (min-width: 78rem) {
    .nav__links {
      display: flex;
      position: static;
      flex-direction: row;
      align-items: center;
      padding-block: 0;
      padding-inline: 0;
      gap: var(--space-3);
    }
  }

  /* Mobile: toggle is visible */
  .nav__toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    inline-size: 2.5rem;
    block-size: 2.5rem;
  }

  /* Desktop: toggle hidden -- matches the 78rem nav__links breakpoint above
     (Session 30: widened from 64rem alongside it -- see that rule's own
     comment for the full reasoning). */
  @media (min-width: 78rem) {
    .nav__toggle {
      display: none;
    }
  }

  /* ─────────────────────────────────────────────
     MAIN CONTENT OFFSET
     Compensate for fixed nav so content isn't hidden.
     ───────────────────────────────────────────── */

  main {
    padding-block-start: var(--nav-height);
  }

  /* ─────────────────────────────────────────────
     SECTION RHYTHM
     Consistent vertical breathing room between sections.
     ───────────────────────────────────────────── */

  .section {
    padding-block: var(--space-16);
  }

  .section--flush-top {
    padding-block-start: 0;
  }

  .section--flush {
    padding-block: 0;
  }

  /* ─────────────────────────────────────────────
     HERO — FULL VIEWPORT
     LCP element lives inside here.
     Grid centers content vertically.
     ───────────────────────────────────────────── */

  .hero {
    /* Subtract nav height so hero is truly full-viewport */
    min-block-size: calc(100dvh - var(--nav-height));
    display: grid;
    /* Two rows: content, then the scroll cue. The cue used to be absolutely
       positioned against the hero's bottom edge, which meant nothing stopped
       tall content from growing into it -- and it did, colliding with the CTA
       buttons on large desktops once the headline took an extra line. Giving
       the cue a real track makes that collision structurally impossible
       rather than merely unlikely at the sizes that happened to get tested.
       Row 2 collapses to zero height wherever the cue is display: none. */
    grid-template-rows: 1fr auto;
    /* --space-6, not --space-4: at --space-4 the cue cleared the CTA by 32px
       at 1600x900, which is separated but still reads as crowded. There is
       ample vertical headroom at every size the cue is shown, so buy the
       breathing room. */
    row-gap: var(--space-6);
    place-items: center;
    /* Was --space-12 (6rem). The hero is a fixed-height box, so padding is
       spent from a hard budget: at --space-12 it took 192px of the 836px
       available at 1440x900, which was part of why the CTA measured 230px
       below the fold before this phase. */
    padding-block: var(--space-8);
    /* Containing block for .hero__watermark, plus the clip that lets the
       crown bleed past the inline-end edge instead of widening the page. */
    position: relative;
    overflow: hidden;
    /* The watermark shimmer uses mix-blend-mode: screen. isolate keeps that
       compositing inside the hero and makes --hero-z-* a local scale. */
    isolation: isolate;
  }

  /* Short viewports (landscape phones, small laptops, 360x640 class devices):
     a locked 100dvh box cannot hold the hero's content, so stop forcing the
     height and let the section grow rather than clipping the CTA. Measured
     at 360x640 before this rule: CTA 137px past the fold. */
  @media (max-height: 43.75rem) {
    .hero {
      min-block-size: auto;
      padding-block: var(--space-6);
    }
  }

  .hero__inner {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* Was --space-6 (48px) between all five children, so nothing grouped and
       the gaps alone spent 160px. --space-4 with .hero__rule carrying the
       optical separation instead. */
    gap: var(--space-4);
    /* No max-inline-size override: .container's own min(--container-max, 100%)
       is the right measure and is what every other section on the page uses.
       This started at --container-narrow (48rem, a prose measure applied to a
       poster), briefly became a bespoke --container-hero (68rem), and is now
       simply the page's standard container so the left text edge is continuous
       from the hero into Doctrine. See the retired token's note in tokens.css. */
    /* Above the watermark and the vignette. */
    position: relative;
    z-index: var(--hero-z-content);
  }

  /* ─────────────────────────────────────────────
     DOCTRINE — STAT ROW
     Three equal columns at desktop, stacked mobile.
     ───────────────────────────────────────────── */

  .doctrine__stats {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }

  @media (min-width: 48rem) {
    .doctrine__stats {
      grid-template-columns: repeat(3, 1fr);
      gap: var(--space-4);
    }
  }

  .doctrine__content {
    display: grid;
    gap: var(--space-8);
  }

  /* ─────────────────────────────────────────────
     DISCIPLINES — RESPONSIVE CARD GRID
     auto-fit so empty cells don't leave ghost columns.
     280px minimum ensures two columns on most tablets.
     ───────────────────────────────────────────── */

  .disciplines__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(var(--discipline-grid-min), 1fr));
    gap: var(--space-4);
  }

  /* Phase 22 continuation: disciplines.php wraps each grid entry's real
     content in an <a class="discipline-card"> so the whole card is a link
     (a <ul> still requires a real <li>, but the <li> itself doesn't need to
     be the flex box). display: contents makes the <li> generate no box of
     its own so its child anchor becomes the actual grid item -- same
     technique already established for a form-in-flex case (admin-table,
     Session 9). Scoped to its own class so it never touches the homepage's
     own <li class="discipline-card"> markup, which still needs to BE the
     flex box, not wrap one. */
  .disciplines__grid-item {
    display: contents;
  }

  /* ─────────────────────────────────────────────
     THE SAGE — TWO-COLUMN AT DESKTOP
     Stacks to single column on mobile.
     ───────────────────────────────────────────── */

  .sage__inner {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-6);
    align-items: start;
  }

  @media (min-width: 56rem) {
    .sage__inner {
      grid-template-columns: 1fr 2fr;
      gap: var(--space-8);
    }
  }

  /* ─────────────────────────────────────────────
     TEASER-INNER — GENERIC TWO-COLUMN EMBLEM + TEXT
     Same grid recipe as .sage__inner above (1fr 2fr at >=56rem, single
     column below it), extracted into its own generic name rather than
     reused directly since .sage__inner refers specifically to The Sage
     section (BEM naming ties a __-scoped class to its block). Second use:
     The Ninety's homepage teaser (index.php), giving it the same visual
     floor every other homepage section already carries (a badge, a card
     grid, an interactive figure, a pull-quote) instead of being the one
     text-only section on the page.
     ───────────────────────────────────────────── */

  .teaser-inner {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-6);
    align-items: start;
  }

  @media (min-width: 56rem) {
    .teaser-inner {
      grid-template-columns: 1fr 2fr;
      gap: var(--space-8);
    }
  }

  /* ─────────────────────────────────────────────
     BIO PAGE INTRO (Phase 18c) — TWO-COLUMN AT DESKTOP
     Same shape as .sage__inner (portrait/emblem column + text column).
     Stacks to single column on mobile.
     ───────────────────────────────────────────── */

  .bio-intro__inner {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-6);
    align-items: start;
  }

  @media (min-width: 56rem) {
    .bio-intro__inner {
      grid-template-columns: 1fr 2fr;
      gap: var(--space-8);
    }
  }

  /* ─────────────────────────────────────────────
     BEGIN — EMAIL CAPTURE
     Centered, narrow, single column.
     ───────────────────────────────────────────── */

  .begin__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-6);
    max-inline-size: 36rem;
    margin-inline: auto;
  }

  .begin__form {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    inline-size: 100%;
  }

  @media (min-width: 32rem) {
    .begin__form {
      flex-direction: row;
    }
  }

  /* ─────────────────────────────────────────────
     SECTION HEADER — REUSABLE
     Heading + optional sub-line above section body.
     ───────────────────────────────────────────── */

  .section-header {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-block-end: var(--space-8);
  }

  .section-header--centered {
    align-items: center;
    text-align: center;
  }

  /* ─────────────────────────────────────────────
     FOOTER LAYOUT
     ───────────────────────────────────────────── */

  .footer__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-4);
    padding-block: var(--space-6);
  }

  @media (min-width: 40rem) {
    .footer__inner {
      flex-direction: row;
      justify-content: space-between;
      text-align: start;
    }
  }

  /* ─────────────────────────────────────────────
     DASHBOARD LAYOUT (Phase 14+)
     Aside sidebar left, content stream right.
     ───────────────────────────────────────────── */

  .dashboard {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-8);
    align-items: start;
    padding-block: var(--space-8);
  }

  @media (min-width: 56rem) {
    .dashboard {
      grid-template-columns: 18rem 1fr;
    }
  }

  /* Phase 23's "Your Standing" widget now lives NESTED inside
     .profile-sidebar (see dashboard.php, Session 30 second pass) rather
     than as its own top-spanning grid row -- the grid-column rule that
     used to live here is gone, not dead code left behind.

     Muscle Balance (this session) is the opposite case: a genuine third
     grid child, spanning full width below the sidebar+stream row. A
     bare third child of a 2-column grid auto-places into column 1 only
     (grid-auto-flow: row fills column 1 then column 2 of the same row,
     then wraps -- it does not span by default), so this explicit span is
     required, not decorative. Session 30 tried and rejected a top-
     spanning full-width row for Standing specifically because a small
     chart left dead space beside it -- that finding doesn't block this
     placement: two real anatomy figures side by side plus a legend and
     caption is substantial enough content to actually fill a row. Harmless
     at the single-column (<56rem) breakpoint too, where there is only one
     column to span. */
  .muscle-balance {
    grid-column: 1 / -1;
  }

  /* "Your Timeline" (this session): same reasoning as .muscle-balance
     directly above -- a genuine third/fourth full-width grid child needs
     an explicit span, since grid-auto-flow only fills column 1 then
     column 2 of the same row by default, it does not span automatically.
     Harmless at the single-column (<56rem) breakpoint, where there is
     only one column to span. */
  .timeline {
    grid-column: 1 / -1;
  }

  /* "Your Track" (Stage 2 of Training Tracks, this session): same
     reasoning as .muscle-balance/.timeline directly above -- a genuine
     full-width grid child needs an explicit span, grid-auto-flow does not
     span automatically. Harmless at the single-column (<56rem)
     breakpoint, where there is only one column to span. */
  .your-track {
    grid-column: 1 / -1;
  }

  /* ─────────────────────────────────────────────
     STREAM HEADER (Phase 17)
     Heading + log-session button on the same row.
     ───────────────────────────────────────────── */

  .stream__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-block-end: var(--space-6);
  }

  /* Phase 22 continuation: groups Browse Disciplines + Log an Entry so
     .stream__header's space-between still resolves to heading-left,
     actions-right with two buttons instead of three space-between items. */
  .stream__header-actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }

  /* ─────────────────────────────────────────────
     AUTH PAGE LAYOUT (Phase 15)
     Single centered card, full viewport height.
     Shared by register, login, forgot-password, reset-password.
     ───────────────────────────────────────────── */

  .auth-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-block-size: calc(100dvh - var(--nav-height));
    padding-block: var(--space-8);
    padding-inline: var(--space-4);
  }

  /* ─────────────────────────────────────────────
     ADMIN PAGE HEADER (Phase 16)
     Heading + action button on the same row.
     ───────────────────────────────────────────── */

  .admin-page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-block-end: var(--space-6);
  }

  /* Admin content form: stacked fields with generous breathing room */
  .admin-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    max-inline-size: var(--container-narrow);
  }

  .admin-form__actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding-block-start: var(--space-2);
  }

  /* Member picker (admin/messages.php, Session 34 continued) -- starts a
     new conversation with a member who has no thread yet. Purely
     structural (flex row, gap, spacing) -- all typography/color comes
     from the existing .form-label/.form-input/.btn classes already used
     everywhere else, so nothing new is introduced here, matching this
     file's own layout-only scope. align-items: flex-end lines the button
     up with the select's bottom edge rather than its own label line. */
  .admin-new-thread {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    margin-block-end: var(--space-8);
  }

  .admin-new-thread .form-group {
    flex: 1;
    max-inline-size: 20rem;
  }

  /* -----------------------------------------------
     SIGNAL FIRE -- directory grid (signals.php).
     auto-fit, same recipe as .disciplines__grid -- empty cells never
     leave ghost columns, and a single visible member still renders as
     one full-width card rather than a narrow orphan.
     ----------------------------------------------- */
  .signals-directory {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--space-4);
    margin-block-end: var(--space-8);
  }

  /* -----------------------------------------------
     MEALS GALLERY (meals.php).
     Card grid, photo-first. Originally auto-fit (same recipe as
     .signals-directory/.disciplines__grid), which is correct for a
     grid reliably populated with many items -- but this gallery grows
     one photo at a time with no schedule (img/meals/README.md), and at
     low real counts (2 published meals, confirmed live) auto-fit's own
     empty-track-collapse behavior let the handful of existing 1fr
     tracks absorb the ENTIRE container width, stretching each card
     (and its fixed 4:3 image) far larger than a gallery thumbnail was
     ever meant to be -- author-reported ("the pictures come out
     long"). Switched to auto-fill (which does NOT collapse empty
     tracks) with a capped max column width -- a card now stays a sane,
     consistent thumbnail size regardless of how many photos exist.

     That auto-fill/minmax approach is GONE now, replaced by explicit
     stepped breakpoints (see below) -- not because it was sized wrong,
     but because a fixed-max-width minmax() track can never guarantee a
     filled row at every viewport width, only at the specific widths
     where the container divides evenly by (max + gap). Author-reported,
     confirmed live via getBoundingClientRect: at 900px viewport, two
     336px-max auto-fill columns filled only 704px of an 836px content
     box, leaving a genuine 132px dead gap beside them -- not a rare
     edge case, but the normal state across the entire ~768-1135px
     range (every tablet, small laptop, and half-a-wide-monitor window
     width), since that whole range sits strictly between "exactly 2
     max-width columns fit" and "exactly 3 do." No minmax()-based
     auto-fill/auto-fit configuration can close this gap while also
     keeping a fixed max column width -- the two goals are structurally
     in tension: a bounded max WILL leave unused space somewhere; an
     unbounded max (1fr) reopens the original stretching bug this file
     already fixed once. The actual fix is to stop asking "how many
     max-width columns fit" and instead fix the COLUMN COUNT per
     breakpoint and let 1fr divide 100% of the row exactly, every time,
     by construction -- the same explicit-breakpoint pattern most
     production gallery/grid systems use for exactly this reason.

     Mobile-first, three tiers, verified live at every boundary before
     shipping:
     - Base (<53rem/848px): 2 columns. Confirmed zero gap from the
       narrowest tested phone width up through 847px (376px columns
       just below the next breakpoint).
     - >=53rem (848px): 3 columns. 53rem is not a round-number guess --
       it is the exact width at which 3 equal 1fr columns first reach
       240px each (3*240 + 2*32 gap = 784px content = 848px viewport),
       which is this project's own already-established thumbnail floor
       (the original minmax() MIN this rule used to carry). Below this
       width, 3 columns would drop under that floor; at and above it,
       they never do. Confirmed live: exactly 240px at 848px, growing
       smoothly (still zero gap at every point, 1fr's own guarantee) up
       to 341px once the container hits its own 72rem/1152px cap (--
       container-max), matching the desktop screenshot the author had
       already reviewed and approved earlier in this same investigation.
     The jump in column width AT each breakpoint (e.g. 376px -> 240px
     right at 848px) is the normal, expected cost of adding a column in
     any stepped grid system -- not a defect. The defect this replaces
     was dead space WITHIN a tier, which 1fr sizing cannot produce.

     min-inline-size: 0 on the grid items remains required at every
     tier, not just the 2-column one -- grid items default to
     min-width: auto, which floors an item at its own content's
     min-content size regardless of column count; without this reset,
     the 2-column split rendered visibly uneven (161.797px / 134px,
     confirmed live) before this rule existed.
     ----------------------------------------------- */
  /* No list-style/padding reset needed here -- reset.css already scopes
     list-style: none to [role="list"], and meals.php's <ul> carries that
     attribute, matching the same convention .disciplines__grid's own
     markup already uses. */
  .meals-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-4);
    margin-block-end: var(--space-8);
  }

  .meals-grid > li {
    min-inline-size: 0;
  }

  @media (min-width: 53rem) {
    .meals-grid {
      grid-template-columns: repeat(3, 1fr);
    }
  }

  /* Toolbar: sort select + meal-type filter pills on one row at rest,
     wrapping to two on narrow viewports -- same shape as
     .stream__header/.admin-page-header, a space-between flex row with
     wrap already proven at this site's own nav-crowding breakpoints. */
  .meals-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-block-end: var(--space-6);
  }

  .meals-toolbar__filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }

  /* Structural half of the sort control -- label and select side by side
     with real spacing between them, rather than the bare inline flow a
     plain <span> wrapper would otherwise leave them in (a <span> doesn't
     establish its own flex context on its own). Typography for the label
     lives in components.css (.meals-toolbar__sort-label), matching this
     file's own split with .meals-filter__pill/.meals-sort. */
  .meals-toolbar__sort {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }

} /* @layer layout */
