/* ==========================================================================
   04-LAYOUT — one canonical grid, inherited by every section.
   No section improvises its own container.
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE SHELL GRID
   Named lines give every section four widths to choose from without any
   section needing to know a pixel value:

   full   |──────────────────────────────────────────────────────|  100%
   bleed  |    ──────────────────────────────────────────────    |  gutter inset
   wide   |        ──────────────────────────────────────        |  1440 max
   content|            ──────────────────────────────            |  1180 max
   text   |                ──────────────────                    |  68ch
   -------------------------------------------------------------------------- */

.l-shell {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
      [bleed-start] minmax(0, calc((var(--w-wide) - var(--w-content)) / 2))
        [wide-start] minmax(0, calc((var(--w-content) - var(--w-text)) / 2))
          /* The gutters must be SUBTRACTED here, not merely declared as tracks.
             This was `min(var(--w-text), 100%)`: on any viewport narrower than
             68ch the content column resolved to the full container width, and
             the two gutter tracks were then added on top of it — so the grid
             came out ~2×gutter wider than the viewport at every phone size.

             `body { overflow-x: hidden }` in 03-base.css hid the scrollbar, so
             it never looked like a bug, but the right edge was genuinely being
             cut: measured on a 390px viewport, 70 elements ran to 406px,
             including the home h1 and every section heading and paragraph. */
          [content-start] min(var(--w-text), 100% - (var(--gutter) * 2)) [content-end]
        minmax(0, calc((var(--w-content) - var(--w-text)) / 2)) [wide-end]
      minmax(0, calc((var(--w-wide) - var(--w-content)) / 2)) [bleed-end]
    minmax(var(--gutter), 1fr) [full-end];
}

/* Every direct child defaults to the content track. */
.l-shell > * { grid-column: content; }

.l-shell > .u-w-text    { grid-column: content; }
.l-shell > .u-w-content { grid-column: wide-start / wide-end; }
.l-shell > .u-w-wide    { grid-column: bleed-start / bleed-end; }
.l-shell > .u-w-bleed   { grid-column: bleed-start / bleed-end; }
.l-shell > .u-w-full    { grid-column: full; }

/* --------------------------------------------------------------------------
   SECTION — vertical rhythm. Sections never set their own block padding.
   -------------------------------------------------------------------------- */

.l-section {
  padding-block: var(--sp-section);
  position: relative;
}

.l-section--tight { padding-block: var(--sp-section-tight); }

/* --------------------------------------------------------------------------
   ADJACENT SECTIONS PAY ONCE, NOT TWICE
   --------------------------------------------------------------------------
   Every section carries symmetric block padding, so each boundary between two
   of them charged the bottom padding of one PLUS the top padding of the next.
   At 1440px that is 86 + 54 = 140px where a normal section meets a tight one,
   and 172px between two normal ones — measured as bands of completely blank
   page in full-page captures:

       /appeals/emergency-relief/   8 bands, 113–182px
       /zakat/                      7 bands, 115–198px
       /zakat/fidya-kaffarah/       4 bands
       /about/, /get-involved/      the same pattern

   Dropping the top padding at the boundary leaves one section's bottom padding
   as the gap — still 86px between major sections, 54px between tight ones —
   which is the rhythm the tokens were chosen for in the first place. The first
   section on a page is unaffected, so the space under the masthead is intact.
   -------------------------------------------------------------------------- */
.l-section + .l-section { padding-block-start: 0; }
.l-section--flush-top    { padding-block-start: 0; }
.l-section--flush-bottom { padding-block-end: 0; }

/* Inner grid for section content, since .l-section sits inside .l-shell. */
.l-section__inner {
  display: grid;
  gap: var(--gap-row);
}

/* Section header: eyebrow + heading + optional lead, used ~20 times.
   The eyebrow belongs TO the heading, so it sits close; the lead is a
   separate thought and gets its own air. A single uniform gap made all three
   read as one undifferentiated block. */
.l-section__head {
  display: grid;
  gap: var(--sp-3);
  max-width: var(--measure);
}

.l-section__head > .c-eyebrow + * { margin-block-start: calc(var(--sp-2) * -1); }
.l-section__head > .t-lead { margin-block-start: var(--sp-2); }

.l-section__head--center {
  justify-items: center;
  text-align: center;
  margin-inline: auto;
}

/* --------------------------------------------------------------------------
   INNER GRID — 12 / 6 / 4 columns
   -------------------------------------------------------------------------- */

.l-grid {
  display: grid;
  gap: var(--gap-col) var(--gap-col);
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

@media (min-width: 768px) {
  .l-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
  .l-grid { grid-template-columns: repeat(12, minmax(0, 1fr)); }
}

/* --------------------------------------------------------------------------
   LAYOUT PRIMITIVES — composable, so components never reinvent flex/grid.
   -------------------------------------------------------------------------- */

/* Vertical stack with a single spacing rule. */
.l-stack {
  display: grid;
  gap: var(--stack-gap, var(--sp-4));
  align-content: start;
}

.l-stack--2 { --stack-gap: var(--sp-2); }
.l-stack--3 { --stack-gap: var(--sp-3); }
.l-stack--5 { --stack-gap: var(--sp-5); }
.l-stack--6 { --stack-gap: var(--sp-6); }
.l-stack--7 { --stack-gap: var(--sp-7); }

/* Horizontal group that wraps rather than overflowing. */
.l-cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cluster-gap, var(--sp-3));
  align-items: var(--cluster-align, center);
}

.l-cluster--2      { --cluster-gap: var(--sp-2); }
.l-cluster--5      { --cluster-gap: var(--sp-5); }
.l-cluster--between { justify-content: space-between; }
.l-cluster--center  { justify-content: center; }
.l-cluster--end     { justify-content: flex-end; }
.l-cluster--baseline { --cluster-align: baseline; }

/* Two-up that collapses to one-up below a content threshold, not a viewport
   one — so it behaves correctly inside narrow columns too. */
.l-switcher {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-col);
}

.l-switcher > * {
  flex-grow: 1;
  flex-basis: calc((var(--switcher-threshold, 34rem) - 100%) * 999);
}

/* Auto-fitting card grid — the workhorse for every card listing. */
.l-cards {
  display: grid;
  gap: var(--gap-col);
  grid-template-columns: repeat(auto-fill, minmax(min(var(--card-min, 20rem), 100%), 1fr));
}

.l-cards--sm { --card-min: 16rem; }
.l-cards--lg { --card-min: 26rem; }

/* Fixed-count grids where the count is meaningful (3 reasons, 4 counters). */
.l-cols-2,
.l-cols-3,
.l-cols-4 {
  display: grid;
  gap: var(--gap-col);
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .l-cols-3,
  .l-cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .l-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
  .l-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .l-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Editorial split: media beside a text column. */
.l-split {
  display: grid;
  gap: var(--gap-col) var(--sp-7);
  align-items: center;
}

@media (min-width: 900px) {
  .l-split { grid-template-columns: 1fr 1fr; }
  .l-split--media-wide { grid-template-columns: 1.25fr 1fr; }
  .l-split--text-wide  { grid-template-columns: 1fr 1.25fr; }
  .l-split--reverse > :first-child { order: 2; }
}

/* Sticky sidebar pattern — appeal detail, donate form. */
.l-sidebar {
  display: grid;
  gap: var(--gap-col) var(--sp-7);
  align-items: start;
}

@media (min-width: 1024px) {
  .l-sidebar { grid-template-columns: minmax(0, 1fr) 22rem; }
  .l-sidebar--wide-aside { grid-template-columns: minmax(0, 1fr) 26rem; }
  .l-sidebar__aside {
    position: sticky;
    top: calc(var(--header-h) + var(--sp-5));
  }
}

/* --------------------------------------------------------------------------
   MEDIA FRAME — fixed aspect box that clips its image, so card hover can
   parallax inside it without the layout moving. Used everywhere.
   -------------------------------------------------------------------------- */

.l-frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-lg);
  background-color: var(--surface-sunken);
  aspect-ratio: var(--frame-ratio, 3 / 2);
}

/* The <picture> has to be stretched, or `height: 100%` on the img below has
   nothing to resolve against. Without this the image keeps its intrinsic height,
   OVERFLOWS the frame and is clipped by the frame's overflow: hidden — so
   object-fit: cover never runs and the crop lands wherever the top-left corner
   happens to fall instead of centred. Measured before the fix: a 21:9 frame at
   1360×583 containing a 1360×765 image, losing 182px off the bottom on 4 pages
   including /where-we-work/ and /impact/.

   This is the third place the same trap has appeared (the home hero and the
   pagehead media were the others). If you write `height: 100%` on an img, check
   what its parent's height actually is. */
.l-frame > picture {
  display: block;
  width: 100%;
  height: 100%;
}

.l-frame > img,
.l-frame > picture > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.l-frame--portrait  { --frame-ratio: 4 / 5; }
.l-frame--square    { --frame-ratio: 1 / 1; }
.l-frame--wide      { --frame-ratio: 16 / 9; }
.l-frame--cinema    { --frame-ratio: 21 / 9; }
.l-frame--tall      { --frame-ratio: 3 / 4; }

/* --------------------------------------------------------------------------
   PAGE SKELETON
   -------------------------------------------------------------------------- */

.l-page {
  display: flex;
  flex-direction: column;
  /* dvh, not vh: on iOS Safari and Chrome Android 100vh is the viewport with the
     URL bar hidden, so a short page sat 60–90px taller than the screen and the
     document scrolled a little with nothing in the gap. dvh tracks the visible
     viewport as the bar collapses. vh stays as the fallback for anything without
     dvh support. */
  min-height: 100vh;
  min-height: 100dvh;
}

.l-main { flex: 1 0 auto; }

/* Room for the mobile sticky donate bar so it never covers the footer. The bar
   pads itself past the home-indicator with env(safe-area-inset-bottom), which
   makes it taller than --donatebar-h on a notched phone — so the reservation has
   to include the inset too, or the last line of the footer sits under it. */
@media (max-width: 1023px) {
  .l-page { padding-block-end: calc(var(--donatebar-h) + env(safe-area-inset-bottom, 0px)); }
}

/* --------------------------------------------------------------------------
   SECTION ALTERNATION — two dark sections never touch (PLAN.md 2A.3).
   -------------------------------------------------------------------------- */

.is-inverted + .is-inverted { padding-block-start: 0; }
