/* ==========================================================================
   Layout: an asymmetric canvas, not a column stack

   The page runs on one continuous 12-track canvas whose outer tracks are the
   gutters themselves. Sections claim uneven spans from that canvas and are
   free to overlap each other on the row axis, which is what produces the
   off-balance composition. Nothing here is a repeating equal-width shelf.
   ========================================================================== */

.l-canvas {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] minmax(0, 0.5fr)
    [main-start] repeat(12, [col] minmax(0, calc(var(--shell-max) / 12)))
    [main-end] minmax(0, 0.5fr)
    [wide-end] minmax(var(--gutter), 1fr)
    [full-end];
}

/* Everything inside the canvas defaults to the readable centre span so a
   stray element can never blow out to full bleed by accident.
 *
 * min-width: 0 is load-bearing, not tidying. A grid item's automatic minimum
 * size is its min-content width, so a single long word or a wide table inside
 * a track will push that track past its stated maximum and blow the whole
 * canvas wider than the viewport. That is invisible at default text size and
 * becomes a horizontal scrollbar at 200%. Resetting the minimum lets the
 * declared track sizes actually govern. */
.l-canvas > * { grid-column: main; min-width: 0; }
.l-canvas > .l-full { grid-column: full; }
.l-canvas > .l-wide { grid-column: wide; }

/* -- Asymmetric span helpers ------------------------------------------------
   Named for what they do compositionally, not for their column count, so the
   ratios can be retuned without renaming every element that uses them.        */
/* The 12 inner tracks each carry a repeated [col] line name, so `col 8` means
   the start of the eighth track. Counting from `main-start` instead would ask
   for the eighth line of that name, only one of which exists, and the grid
   would resolve the rest as implicit columns off the right edge. */
.l-span-lead    { grid-column: main-start / span 7; }
.l-span-counter { grid-column: col 8 / main-end; }
.l-span-offset  { grid-column: col 3 / main-end; }
.l-span-inset   { grid-column: col 2 / span 8; }
.l-span-narrow  { grid-column: main-start / span 5; }
.l-span-tail    { grid-column: col 6 / main-end; }

/* Page-specific pairings. */
.l-span-rail    { grid-column: main-start / span 3; }   /* marketplace filters */
.l-span-results { grid-column: col 4 / main-end; }
.l-span-media   { grid-column: main-start / span 7; }   /* listing gallery */
.l-span-detail  { grid-column: col 9 / main-end; }

/* Below the desktop breakpoint every asymmetric span collapses to the full
   readable column. Off-balance composition is a large-screen luxury; on a
   handset it just makes text columns too narrow to read. */
@media (max-width: 63.99em) {
  .l-span-lead, .l-span-counter, .l-span-offset,
  .l-span-inset, .l-span-narrow, .l-span-tail,
  .l-span-rail, .l-span-results, .l-span-media, .l-span-detail { grid-column: main; }
}

/* .l-prose no longer caps its width. Content spans 100% of its container
   across this site; the container is what sets the line length, and a column
   that wants shorter lines should be a narrower column. */
.l-prose { width: 100%; }

/* -- Deliberate overlap ----------------------------------------------------
   Two elements sharing a grid row, one pulled up into the other, is the core
   move of the hero and the brand header. Implemented with explicit row
   placement rather than negative margins so it stays predictable when the
   text scale changes.                                                        */
.l-overlap { display: grid; grid-template-rows: [over-start] auto [over-mid] auto [over-end]; }
.l-overlap > .l-overlap__under { grid-row: over-start / over-end; grid-column: 1 / -1; }
.l-overlap > .l-overlap__over  { grid-row: over-mid / over-end; grid-column: 1 / -1; align-self: end; }

/* -- Section rhythm --------------------------------------------------------
   Sections alternate their internal offset direction, which keeps a long
   scroll from settling into a predictable left-aligned drumbeat.             */
.l-section { padding-block: var(--space-2xl); position: relative; }
.l-section--tight { padding-block: var(--space-xl); }
.l-section--tall  { padding-block: var(--space-3xl); }

.l-section--inverse {
  background: var(--bg-inverse);
  color: var(--ink-invert);
}
.l-section--inverse :is(h1, h2, h3, h4) { color: #FFFFFF; }
.l-section--inverse a { color: var(--azure-300); }

.l-section--sunken { background: var(--bg-sunken); }

/* A hairline that leans at the wordmark's angle. Used as a section marker in
   place of a conventional horizontal rule. */
.l-section__marker {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--gutter);
  width: clamp(3rem, 7vw, 6rem);
  height: 3px;
  background: var(--brand-azure);
  transform: skewX(var(--lean));
  transform-origin: left center;
}

/* -- Flow spacing -----------------------------------------------------------
   One owl selector instead of margin declarations scattered across every
   component. Density is tuned per context by resetting the custom property.  */
.l-flow > * + * { margin-block-start: var(--flow-space, var(--space-s)); }
.l-flow--loose { --flow-space: var(--space-l); }
.l-flow--tight { --flow-space: var(--space-2xs); }

/* -- Rails ------------------------------------------------------------------
   A horizontally scrolling rail on small screens that resolves into an
   asymmetric static composition once there is room. Cards are intentionally
   unequal in width so the row never reads as a template.                     */
.l-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(min(78vw, 20rem), 1fr);
  gap: var(--space-s);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: inline mandatory;
  scroll-padding-inline: var(--gutter);
  padding-block-end: var(--space-xs);
  /* Bleed the rail through the page gutter so it reads as continuing past
     the edge rather than stopping politely inside it. */
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  scrollbar-width: thin;
}
.l-rail > * { scroll-snap-align: start; }

@media (min-width: 64em) {
  .l-rail {
    grid-auto-flow: row;
    grid-template-columns: 1.35fr 1fr 1fr;
    overflow: visible;
    margin-inline: 0;
    padding-inline: 0;
  }
  .l-rail--four   { grid-template-columns: 1.4fr 1fr 1fr 1.2fr; }
  .l-rail--split  { grid-template-columns: 1.7fr 1fr; }
  .l-rail--thirds { grid-template-columns: 1fr 1.25fr 1fr; }
}

/* -- Auto-fitting collections ----------------------------------------------
   For genuinely uniform sets (listing results), where irregularity would
   fight the user's need to compare items side by side.                       */
.l-results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr));
  gap: var(--space-m) var(--space-s);
}

/* -- Two-column article shell ---------------------------------------------- */
.l-editorial { display: grid; gap: var(--space-l); }

@media (min-width: 64em) {
  .l-editorial {
    grid-column: main-start / main-end;
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    gap: var(--space-xl);
    align-items: start;
  }
  .l-editorial__aside { position: sticky; top: calc(var(--header-h, 76px) + var(--space-m)); }
  /* Pull-quotes and tables span the full prose track. */
  .l-editorial__break { grid-column: 2 / 3; }
}

/* -- Small utilities -------------------------------------------------------- */
.u-cluster { display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-xs); align-items: center; }
.u-cluster--between { justify-content: space-between; }
.u-stack { display: flex; flex-direction: column; }
.u-push { margin-inline-start: auto; }
.u-center { margin-inline: auto; }

@media (max-width: 63.99em) { .u-desktop-only { display: none !important; } }
@media (min-width: 64em)    { .u-mobile-only  { display: none !important; } }

/* -- Scroll-triggered entrance ---------------------------------------------
   The class is added by app.js via IntersectionObserver. The initial hidden
   state is applied only when JS is present, so a no-JS visitor sees content
   rather than a page of permanently invisible sections.                      */
/* Gated on .js-reveal, not .js. The head bootstrap adds that class and starts a
   watchdog; if app.js fails to load or throws before registering, the watchdog
   removes it and every section becomes visible. Hiding content behind a class
   that only a successful script can undo is how a decorative animation turns
   into a blank page. */
html.js-reveal .u-reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
html.js-reveal .u-reveal.is-revealed { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html.js-reveal .u-reveal { opacity: 1; transform: none; transition: none; }
}

/* Printing a page of invisible sections is not acceptable either. */
@media print {
  .u-reveal { opacity: 1 !important; transform: none !important; }
}

/* --------------------------------------------------------------------------
   Policy and standards pages

   A contents rail plus a measure-capped prose column. The rail is sticky so a
   long policy stays navigable, and it collapses away below the breakpoint
   rather than pushing the text it is supposed to help you read off the screen.
   -------------------------------------------------------------------------- */
.l-policy { display: grid; gap: var(--space-l); grid-column: main; }

@media (min-width: 64em) {
  .l-policy {
    grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
    gap: var(--space-2xl);
    align-items: start;
  }
  .l-policy__aside {
    position: sticky;
    top: calc(var(--header-h, 76px) + var(--space-m));
    max-height: calc(100dvh - var(--header-h, 76px) - var(--space-xl));
    overflow-y: auto;
  }
}

/* --------------------------------------------------------------------------
   Adjacent spans need a gutter

   .l-canvas has no column gap by design: a full-bleed section must be able to
   touch the viewport edge. That means two spans placed side by side butt
   straight up against each other, which is why the marketplace filter rail sat
   flush against the first card. The gutter belongs on the pairs that are
   actually adjacent, not on the grid.
   -------------------------------------------------------------------------- */
@media (min-width: 64em) {
  .l-span-rail  { padding-right: var(--space-l); }
  .l-span-media { padding-right: var(--space-l); }
  .l-span-lead  { padding-right: var(--space-m); }
}

/* Card rows: equal columns so every card shows the same amount of picture.
   Distinct from .l-rail, which is deliberately asymmetric and belongs where
   three panels carry different weights of content rather than the same thing
   repeated. */
.l-cards {
  display: grid;
  gap: var(--space-m);
  grid-template-columns: 1fr;
  align-items: stretch;
}
@media (min-width: 48em) {
  .l-cards--articles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 76em) {
  .l-cards--articles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
