@layer exsa.layouts {

/* ============================================================
   EXSA Layout — the paper model (general)

   Link AFTER exsa.css + your theme.
   Element-driven: the markup IS the declaration — no body classes.

   ── The paper model ──
   body              = the table — full viewport boundary; nothing
                       required here
   .layout__page     = the sheet of paper laid on it. Full width by
                       default; set --layout-page-w to any smaller
                       size (e.g. 80%) and it centers on the table.
                       It carries its own look (surface tokens).
   .layout__grid     = the page's container layer — named zones cut
                       from the sheet, side pieces sized to fill the
                       sheet's width 100% exactly
   elements/components = what goes inside each piece; they fit the
                       piece (components own their look, the layout
                       only owns the pieces)

   ── Zones (inside .layout__grid, all optional) ──
   .layout__topbar    = in-page topbar — a strip above the grid,
                        sticky to the viewport edge. Pair it with
                        the .topbar component for its chrome.
                        (Or keep the topbar OUTSIDE the page:
                        use topbar.css's .topbar + body.has-topbar —
                        the sheet then sits below the full-bleed bar.
                        Both placements work at once.)
   .layout__announce  = fixed full-bleed strip above everything
                        (outside the sheet) — pairs with the outside
                        topbar; body padding and sticky offsets
                        account for it automatically
   .layout__header    = full-width band above the pieces
   .layout__aside-start = side piece at the inline-start edge
   .layout__content   = the main piece (1fr — takes all leftover)
   .layout__aside-end = side piece at the inline-end edge
   .layout__footer    = full-width band below the pieces

   ── Mode modifiers (optional, composable) ──
   .layout__hero             = full-width hero band above the pieces
   .layout__page--auto       = sheet sizes to content (sticky footer off)
   .layout__page--frame      = outside topbar + footer adopt the
                               sheet's width (one shared edge).
                               Set --layout-page-w on :root or
                               <body> — the outside chrome is a
                               sibling, so it reads the token there,
                               not from the page element.
   .layout__page--app        = dashboard-style app frame — ships in
                               layouts/dashboard.css (link AFTER
                               general.css): the sheet is exactly
                               the viewport; .layout__aside-start is
                               a fixed full-height rail and
                               .layout__content the scrolling pane.
   .layout__grid--centered   = content piece reads --layout-measure
   .layout__aside-start--sticky = side piece sticks while scrolling
   .layout__aside-end--sticky   = (clears the topbar automatically)
   .sidebar--collapsed on a side piece = icon-width track (auto,
   matching the rail's own width — --sidebar-collapsed-w, 56px)

   Piece count is structural: content alone = 1 piece; one aside =
   2; both asides = 3; header/footer frame them. Any N-way split
   inside the content piece comes from the utilities grid (ONE grid
   system: .grid → .grid-cols-1…8).

   DOM order recommendation: topbar (if in-page), hero, header,
   aside-start, content, aside-end, footer.

   ── Space model ──
   Zones are element-evidenced (:has()): the grid only reserves
   side tracks for asides that actually exist, a collapsed sidebar
   (sidebar--collapsed) shrinks its track to icon width, and an
   absent header/footer band collapses to nothing. Unused space
   flows into .layout__content.

   ── Responsiveness ──
   The sheet is a query container (@container exsa-page). At
   ≤860px the pieces stack to one column — keyed to the SHEET's
   width, not the viewport's, so the layout stays correct when
   embedded (app pane, split view, iframe). A viewport media
   query mirrors the same rules as the fallback for engines
   without container queries.

   ── Tokens (override on the page element or in your theme) ──
   --layout-page-w            sheet width (default 100%; e.g. 80%)
   --layout-page-min-h        sheet height (default 100vh/100dvh —
                              footer pinned to the sheet bottom;
                              set auto to size to content)
   --layout-page-bg           var(--surface-bg)    sheet paper color
   --layout-page-border       var(--surface-border) sheet edge
   --layout-page-radius       var(--surface-radius) sheet corners
   --layout-page-shadow       var(--surface-shadow-lg) sheet elevation
   --layout-page-pad          0                    sheet inner padding
   --layout-aside-start-w     clamp(220px, 25%, 300px) start piece
   --layout-aside-end-w       clamp(220px, 25%, 300px) end piece
   --layout-gap               spacing between pieces
   --layout-hero-min-h        55vh    hero band height
   --layout-measure           var(--width-content) centered content
                              width (--grid--centered)
   --layout-sticky-top        sticky offset — auto-syncs to the
                              topbar height when a topbar is present
   --layout-announce-h        40px    announce strip height

   Semantics: <header> for .layout__header, <aside> for the side
   pieces, <main> for .layout__content, <footer> for .layout__footer,
   <header class="topbar"> for the in-page topbar.
   ============================================================ */


:root {
  --layout-page-w: 100%;
  --layout-page-min-h: 100vh;
  --layout-page-bg: var(--surface-bg);
  --layout-page-border: var(--surface-border);
  --layout-page-radius: var(--surface-radius);
  --layout-page-shadow: var(--surface-shadow-lg);
  --layout-page-pad: 0;
  --layout-aside-start-w: clamp(220px, 25%, 300px);
  --layout-aside-end-w: clamp(220px, 25%, 300px);
  --layout-gap: calc(16px * var(--space-factor, 1));
  --layout-hero-min-h: 55vh;
  --layout-measure: var(--width-content);
  --layout-sticky-top: 0px;
  --layout-announce-h: 40px;
}

/* ════════════════════════════════════════
   1. PAGE — the sheet of paper
   ════════════════════════════════════════ */
.layout__page {
  width: var(--layout-page-w);
  max-width: 100%;
  margin-inline: auto;
  min-height: var(--layout-page-min-h);
  background: var(--layout-page-bg);
  border: var(--layout-page-border);
  border-radius: var(--layout-page-radius);
  box-shadow: var(--layout-page-shadow);
  padding: var(--layout-page-pad);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
}
/* Dynamic-viewport height where supported — the sheet tracks the
   visible viewport (mobile URL bars) instead of the large viewport. */
.layout__page { min-height: var(--layout-page-min-h, 100dvh); }

/* The sheet is the responsive context — pieces stack against the
   sheet's own width, not the window's. */
.layout__page { container-type: inline-size; container-name: exsa-page; }

/* Sticky-offset sync — sticky side pieces clear the fixed bar
   (outside topbar) or the in-page sticky topbar. */
body:has(> .topbar) .layout__page { --layout-sticky-top: var(--topbar-height, 56px); }
.layout__page:has(> .layout__topbar) { --layout-sticky-top: var(--topbar-height, 56px); }

/* Frame mode — the outside full-bleed topbar and the footer adopt
   the sheet's width, so page chrome and paper share one edge.
   The outside chrome is a SIBLING of the sheet, so it reads
   --layout-page-w from a shared ancestor (:root / body) — an
   inline override on the page element alone can't reach it. */
body:has(> .layout__page--frame) > .topbar,
body:has(> .layout__page--frame) > .footer {
  width: var(--layout-page-w, 100%);
  max-width: 100%;
  margin-inline: auto;
}

/* Content-sized sheet — sticky footer off (default pins the footer
   to the sheet bottom via --layout-page-min-h). */
.layout__page--auto { --layout-page-min-h: auto; }

/* ════════════════════════════════════════
   2. IN-PAGE TOPBAR — a piece above the grid
   Pair with the .topbar component class for its chrome. The inset
   reset drops the component's fixed-position offsets so the sticky
   constraint stays purely vertical.
   ════════════════════════════════════════ */
.layout__page > .layout__topbar {
  position: sticky;
  top: 0;
  inset-inline: auto;
  z-index: var(--z-topbar, 10);
}
.layout__page > .layout__topbar + .layout__grid {
  margin-block-start: var(--layout-gap);
}
.layout__page > .layout__topbar + .layout__hero {
  margin-block-start: var(--layout-gap);
}

/* ════════════════════════════════════════
   3. ANNOUNCE — fixed strip above everything
   Full-bleed chrome OUTSIDE the sheet (sibling of the page), like
   the outside topbar. Body padding, the topbar's position, and the
   sticky pieces' offset all account for it automatically. The link
   color + white text are the canonical announcement look; retoken
   in your theme to match a store's brand.
   ════════════════════════════════════════ */
.layout__announce {
  position: fixed;
  top: 0; inset-inline-start: 0; inset-inline-end: 0;
  z-index: var(--z-announce, 95);
  height: var(--layout-announce-h);
  display: flex; align-items: center; justify-content: center;
  gap: calc(8px * var(--space-factor, 1));
  padding: 0 var(--gap);
  background: var(--color-link);
  color: var(--color-button-text, #fff);
  font-size: 0.82rem;
  font-weight: 600;
  text-align: center;
}
.layout__announce a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Body reservation — announce only */
body:has(> .layout__announce):not(:has(> .topbar)) {
  padding-top: var(--layout-announce-h);
}
/* Body reservation — announce + outside topbar */
body.has-topbar:has(.topbar):has(> .layout__announce) {
  padding-top: calc(var(--topbar-height, 56px) + var(--layout-announce-h));
}
/* The fixed topbar sits below the announce strip */
body:has(> .layout__announce) > .topbar { top: var(--layout-announce-h); }
/* Sticky pieces clear both fixed bars */
body:has(> .layout__announce) .layout__page {
  --layout-sticky-top: calc(var(--topbar-height, 0px) + var(--layout-announce-h));
}

/* ════════════════════════════════════════
   4. HERO — full-width band above the pieces
   Pair with components/hero.css (or a classless <header>) for
   the look — the layout only owns the band's place + height.
   ════════════════════════════════════════ */
.layout__page > .layout__hero { min-height: var(--layout-hero-min-h); }
.layout__page > .layout__hero + .layout__grid {
  margin-block-start: var(--layout-gap);
}

/* ════════════════════════════════════════
   5. GRID — the pieces (named zones, element-evidenced)
   Side tracks exist only when their aside element does, and a
   sidebar--collapsed piece shrinks its track to icon width (auto).
   The header/footer bands are always declared and collapse to
   nothing when absent. Rows: bands auto, content 1fr — with a
   full-height sheet the footer pins to the bottom.
   ════════════════════════════════════════ */
.layout__grid {
  display: grid;
  flex: 1 1 auto;
  min-height: 0;
  grid-template-columns: 1fr;
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "header"
    "content"
    "footer";
  column-gap: var(--layout-gap);
}
.layout__grid > * { min-width: 0; }

/* One start aside → 2 pieces */
.layout__grid:has(> .layout__aside-start:not(.sidebar--collapsed)):not(:has(> .layout__aside-end)) {
  grid-template-columns: var(--layout-aside-start-w) 1fr;
  grid-template-areas:
    "header      header"
    "aside-start content"
    "footer      footer";
}
.layout__grid:has(> .layout__aside-start.sidebar--collapsed):not(:has(> .layout__aside-end)) {
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "header      header"
    "aside-start content"
    "footer      footer";
}
/* One end aside → 2 pieces */
.layout__grid:has(> .layout__aside-end:not(.sidebar--collapsed)):not(:has(> .layout__aside-start)) {
  grid-template-columns: 1fr var(--layout-aside-end-w);
  grid-template-areas:
    "header      header"
    "content     aside-end"
    "footer      footer";
}
.layout__grid:has(> .layout__aside-end.sidebar--collapsed):not(:has(> .layout__aside-start)) {
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "header      header"
    "content     aside-end"
    "footer      footer";
}
/* Both asides → 3 pieces (each side keeps or drops its track
   independently when sidebar--collapsed) */
.layout__grid:has(> .layout__aside-start:not(.sidebar--collapsed)):has(> .layout__aside-end:not(.sidebar--collapsed)) {
  grid-template-columns: var(--layout-aside-start-w) 1fr var(--layout-aside-end-w);
  grid-template-areas:
    "header      header      header"
    "aside-start content     aside-end"
    "footer      footer      footer";
}
.layout__grid:has(> .layout__aside-start.sidebar--collapsed):has(> .layout__aside-end:not(.sidebar--collapsed)) {
  grid-template-columns: auto 1fr var(--layout-aside-end-w);
  grid-template-areas:
    "header      header      header"
    "aside-start content     aside-end"
    "footer      footer      footer";
}
.layout__grid:has(> .layout__aside-start:not(.sidebar--collapsed)):has(> .layout__aside-end.sidebar--collapsed) {
  grid-template-columns: var(--layout-aside-start-w) 1fr auto;
  grid-template-areas:
    "header      header      header"
    "aside-start content     aside-end"
    "footer      footer      footer";
}
.layout__grid:has(> .layout__aside-start.sidebar--collapsed):has(> .layout__aside-end.sidebar--collapsed) {
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    "header      header      header"
    "aside-start content     aside-end"
    "footer      footer      footer";
}

/* ════════════════════════════════════
   6. ZONES + MODES
   ════════════════════════════════════ */
.layout__grid > .layout__header { grid-area: header; }
.layout__grid > .layout__content { grid-area: content; min-width: 0; }
.layout__grid > .layout__footer { grid-area: footer; }
.layout__grid > .layout__aside-start { grid-area: aside-start; }
.layout__grid > .layout__aside-end { grid-area: aside-end; }

/* The track owns the side piece's width — this overrides fixed
   component widths (e.g. .sidebar's 240px) so the piece fills its
   track exactly. A collapsed piece is exempt: it keeps the rail
   width from .sidebar--collapsed (--sidebar-collapsed-w, 56px), and
   its auto track then matches that rail width exactly. */
.layout__grid > .layout__aside-start:not(.sidebar--collapsed),
.layout__grid > .layout__aside-end:not(.sidebar--collapsed) {
  width: auto;
}

/* Vertical rhythm — bands carry the gap so an absent band leaves
   no stray spacing (empty grid rows would still consume row-gap). */
.layout__grid > .layout__header { margin-block-end: var(--layout-gap); }
.layout__grid > .layout__footer { margin-block-start: var(--layout-gap); }

/* Sticky side pieces — clear the topbar via --layout-sticky-top
   (auto-synced above from either topbar placement). */
.layout__aside-start--sticky,
.layout__aside-end--sticky {
  position: sticky;
  top: var(--layout-sticky-top);
  align-self: start;
  max-height: calc(100vh - var(--layout-sticky-top));
  max-height: calc(100dvh - var(--layout-sticky-top));
  overflow-y: auto;
}

/* Centered content piece — reading width from --layout-measure. */
.layout__grid--centered > .layout__content {
  max-width: var(--layout-measure);
  width: 100%;
  margin-inline: auto;
}

/* ════════════════════════════════════
   7. STACKING — pieces go single-column when the SHEET is ≤860px.
   The viewport media query is the fallback for engines without
   container queries (same rules, keyed to the window). The
   @container block wins wherever both apply and stays correct
   when the sheet is smaller than the viewport.
   ════════════════════════════════════ */
@media (max-width: 860px) {
  .layout__grid {
    display: flex;
    flex-direction: column;
  }
  .layout__grid > * + * { margin-block-start: var(--layout-gap); }
  .layout__grid > .layout__header { margin-block-end: 0; }
  .layout__grid > .layout__footer { margin-block-start: 0; }
  /* A collapsed rail keeps its icon width at any size — the 100%
     reset applies to expanded pieces only. */
  .layout__grid > .layout__aside-start:not(.sidebar--collapsed),
  .layout__grid > .layout__aside-end:not(.sidebar--collapsed) {
    width: 100%; flex-basis: 100%;
  }
  .layout__aside-start--sticky,
  .layout__aside-end--sticky {
    position: static;
    max-height: none;
    overflow: visible;
  }
}

@container exsa-page (max-width: 860px) {
  .layout__grid {
    display: flex;
    flex-direction: column;
  }
  .layout__grid > * + * { margin-block-start: var(--layout-gap); }
  .layout__grid > .layout__header { margin-block-end: 0; }
  .layout__grid > .layout__footer { margin-block-start: 0; }
  /* A collapsed rail keeps its icon width at any size — the 100%
     reset applies to expanded pieces only. */
  .layout__grid > .layout__aside-start:not(.sidebar--collapsed),
  .layout__grid > .layout__aside-end:not(.sidebar--collapsed) {
    width: 100%; flex-basis: 100%;
  }
  .layout__aside-start--sticky,
  .layout__aside-end--sticky {
    position: static;
    max-height: none;
    overflow: visible;
  }
}

}
