/* grid.css -- the house grid: 24 columns, a 3px line, one margin for every page.
 *
 * COLUMNS. 24 on a desktop, 12 below 1024px -- 12 divides 24, so a half is still a half
 * and a third still a third on a phone. 30 was considered and lost to the pages it is for:
 * calsans splits its sections 5:7 three times, which is 10 | 14 here and 12.5 | 17.5 on 30.
 * The gutter is the pad scale's 24px (12 on a phone). The margin is the case study's own --
 * clamp(20px, 5vw, 40px) on a phone, clamp(32px, 7vw, 112px) from 681px -- and it is the
 * SAME for every project: page padding belongs to the grid, not to the page.
 *
 * LINES. A 3px unit: the leading subdivided, the way 12/15 sits on a 3pt grid. Body is
 * 16/24. A leading is a whole number of units -- for fluid type, round(nearest, Nem,
 * var(--bl)) gives that at every size the clamp() produces -- and so is the vertical space
 * between text blocks in a column, because that decides where the next baseline lands.
 * The pad scale is NOT on the line and need not be: a component's insides are its own.
 * What carries text in the flow (a pill, a chip, a button) is the exception: its HEIGHT is
 * in units, so a row of them is a line of type and nothing below it is knocked off.
 *
 * CARDS (rule A, Mark 2026-10-01). A card is a box on the grid; its contents step in one
 * column + one gutter, so they land on the next column line.
 *
 *   .wm-grid            columns + the page margin (a whole page or section)
 *   .wm-cols            the same columns, no margin -- inside a wrapper that has it
 *   --span / --start    a child's columns on the 24 (default: all of them)
 *   --span-md / --start-md   the same below 1024px (default: all 12)
 *   .wm-card            a box whose contents start one column + one gutter in
 *   --grid-col          one column's width, as a length, inside either container
 *   --bl                the line; .wm-lines puts text's baselines on it (below)
 *   --snap              the remainder a builder or gridSnap.js measured (below)
 *   --row-step          a .wm-baselines row's smallest text lead, on its items (gridSnap.js)
 *
 * PAGES AND TOOLS (Mark 2026-10-04). A page has the margin; a tool has none, ever.
 *   .wm-grid--bleed     no margin: on a .wm-grid, or once on main/body for a whole app.
 *                       Columns run the full width, the gutter stays, and a box touching a
 *                       window edge holds its text one gutter in from it.
 *   .wm-break           a child of a margined grid whose BOX runs to both window edges; its
 *   .wm-break--left     text stays at the margin (a bleed of the box, not of the words).
 *   .wm-break--right    One edge only, on the child's own --span. [data-nosnap]: a picture
 *                       or stage, no inner padding.
 *   [data-shot="mac"]   a macOS window screenshot (img, video, or a picture): the shadow
 *                       margin is cut out of the layout so the WINDOW's edge sits on the
 *                       column edge. --shot-mac-l/r/t/b are the shadow in capture px. */
@layer wm.grid {
  :root {
    --grid-cols: 24;
    --grid-gutter: 24px;
    --grid-margin: clamp(20px, 5vw, 40px);
    --bl: 3px;
  }
  @media (min-width: 681px)  { :root { --grid-margin: clamp(32px, 7vw, 112px); } }
  @media (max-width: 1023px) { :root { --grid-cols: 12; } }
  @media (max-width: 599px)  { :root { --grid-gutter: 12px; } }

  .wm-grid, .wm-cols {
    /* A container, so anything inside can measure a column in cqw without JS. */
    container-type: inline-size;
    display: grid;
    grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
    column-gap: var(--grid-gutter);
  }
  .wm-grid { padding-inline: var(--grid-margin); }
  :is(.wm-grid, .wm-cols) > * {
    /* Declared and unset (`initial`), so the fallbacks apply until a host sets them on the
       child -- the same move as chip.css's --chip-color. */
    --span: initial; --start: initial; --span-md: initial; --start-md: initial;
    min-width: 0;
    grid-column: var(--start, auto) / span var(--span, 24);
    /* One column, as a length. 100cqw is the container's CONTENT box, so .wm-grid's
       padding-inline is already out of it: only the gutters come off. */
    --grid-col: calc((100cqw - (var(--grid-cols) - 1) * var(--grid-gutter)) / var(--grid-cols));
  }
  @media (max-width: 1023px) {
    :is(.wm-grid, .wm-cols) > * { grid-column: var(--start-md, auto) / span var(--span-md, 12); }
  }
  .wm-card { padding: var(--grid-gutter) calc(var(--grid-col) + var(--grid-gutter)); }

  /* GRID BREAKERS. A child of a margined .wm-grid that runs to the window edge: the box takes
     the margin back (negative margin-inline by --grid-margin) and pads it back in, so text in a
     breaker still starts at the margin and a .wm-cols inside it lands on the page's own
     columns -- its content box IS the page's column area. A breaker of two edges spans
     everything; a one-edge breaker keeps the child's --span (--span-md below 1024) and runs to
     the side it breaks. A picture or stage (data-nosnap) gets no inner padding: it is the box.
     With both classes, the side the modifier names wins. In a bleed root there is no margin
     to break: a breaker is an ordinary child there, and the bleed rule below pads it. */
  .wm-grid > .wm-break { grid-column: 1 / -1; margin-inline: calc(-1 * var(--grid-margin)); padding-inline: var(--grid-margin); }
  .wm-grid > .wm-break--left { grid-column: 1 / span var(--span, 24); margin-inline: calc(-1 * var(--grid-margin)) 0; padding-inline: var(--grid-margin) 0; }
  .wm-grid > .wm-break--right { grid-column: span var(--span, 24) / -1; margin-inline: 0 calc(-1 * var(--grid-margin)); padding-inline: 0 var(--grid-margin); }
  .wm-grid > :is(.wm-break, .wm-break--left, .wm-break--right):where([data-nosnap]) { padding-inline: 0; }

  /* TOOLS BLEED: .wm-grid--bleed. font-proofer, ReCal: no margin, ever. On one .wm-grid, or once
     on main / body so every grid inside opts in (--grid-margin is inherited; on <html> a
     host's own unlayered :root --grid-margin would beat it, so prefer main or body). The
     columns run edge to edge, still 24 / 12, gutter kept -- and text never sits flush against
     the window: a box that touches an edge pads one gutter on that side. Which boxes touch an
     edge is read from the house placement, not guessed from the DOM:
       start pad = gutter x max(0, 2 - start)                      1 when it starts in column 1
       end pad   = gutter x max(0, start + span - cols)            1 when it ends in the last
     An auto-placed child counts as starting in column 1: give one that starts elsewhere its
     --start (--start-md). A stage (data-nosnap) or a picture is not padded: it keeps its own room. The pad is
     layered, so a host's own unlayered padding on the box replaces it -- put that padding on
     an inner element instead. */
  .wm-grid--bleed { --grid-margin: 0px; }
  :is(.wm-grid--bleed .wm-grid, .wm-grid.wm-grid--bleed) > :not(:where([data-nosnap], [data-shot], img, video, picture, canvas, svg)) {
    padding-inline-start: calc(var(--grid-gutter) * max(0, 2 - var(--start, 1)));
    padding-inline-end: calc(var(--grid-gutter) * max(0, var(--start, 1) + var(--span, 24) - var(--grid-cols)));
  }
  @media (max-width: 1023px) {
    .wm-grid > .wm-break--left { grid-column: 1 / span var(--span-md, 12); }
    .wm-grid > .wm-break--right { grid-column: span var(--span-md, 12) / -1; }
    :is(.wm-grid--bleed .wm-grid, .wm-grid.wm-grid--bleed) > :not(:where([data-nosnap], [data-shot], img, video, picture, canvas, svg)) {
      padding-inline-start: calc(var(--grid-gutter) * max(0, 2 - var(--start-md, 1)));
      padding-inline-end: calc(var(--grid-gutter) * max(0, var(--start-md, 1) + var(--span-md, 12) - var(--grid-cols)));
    }
  }

  /* MACOS WINDOW SCREENSHOTS: [data-shot="mac"]. Cmd-Shift-4, space, and the window comes with
     its drop shadow as a transparent margin, so the visible window edge sits INSIDE the image
     and a shot set on the columns lands its window short of them. The margin is a fixed size
     in points, not a ratio: measured on the case study's shots (2026-10-04, alpha >= 250), a 2x
     capture carries 112 px left and right, 76 on top and 148 below (56 / 38 / 74 pt; the
     shadow falls downward). A file scaled after capture keeps the same FRACTIONS of its own
     width as the capture had of its, so it says by how much in --shot-scale (file px per
     capture px: the case study's 2000px files are 0.643 of a ~3111px capture, 72 / 2000).
     A 1x capture is --shot-scale: 0.5.

     The arithmetic. B is the box the WINDOW should fill (the containing block, 100%), w the
     file's width in px (--shot-w), and l r t b the shadow in file px (token x scale). The
     window is w - l - r file px wide, so one file px renders at k = B / (w - l - r), and:
       width  = k x w                            the whole file, shadow included
       margin = -k x t   -k x r   -k x b   -k x l
     Percent margins resolve against the containing block's INLINE size on all four sides,
     the top and bottom too -- which is exactly B, so every term is 100% x n / (w - l - r)
     and the image's layout box is the window, to the pixel, at any width. The shadow is
     drawn past the box like any shadow; a margined .wm-grid holding a shot clips its own
     sides (overflow-x: clip, not a scroll container) so a phone's 20px margin, narrower
     than the shadow, does not scroll the page sideways.

     --shot-w needs the file's pixel width and CSS cannot read it: gridSnap.js writes it
     inline (from the loaded image, the width attribute until then), or the page does. The
     rules apply only once it is there -- [style*="--shot-w"] -- so a page without the
     script shows the plain image, never one sized from a guess. */
  :root { --shot-mac-l: 112; --shot-mac-r: 112; --shot-mac-t: 76; --shot-mac-b: 148; }
  [data-shot="mac"] { --shot-w: initial; --shot-scale: 1; }
  picture[data-shot="mac"] { display: block; }
  :is(img, video)[data-shot="mac"][style*="--shot-w"], picture[data-shot="mac"][style*="--shot-w"] > img {
    --_shot-k: calc(100% / (var(--shot-w) - (var(--shot-mac-l) + var(--shot-mac-r)) * var(--shot-scale)));
    display: block;
    max-width: none;
    height: auto;
    width: calc(var(--_shot-k) * var(--shot-w));
    margin: calc(var(--_shot-k) * var(--shot-mac-t) * var(--shot-scale) * -1)
            calc(var(--_shot-k) * var(--shot-mac-r) * var(--shot-scale) * -1)
            calc(var(--_shot-k) * var(--shot-mac-b) * var(--shot-scale) * -1)
            calc(var(--_shot-k) * var(--shot-mac-l) * var(--shot-scale) * -1);
  }
  .wm-grid:has([data-shot="mac"]) { overflow-x: clip; }

  /* THE ROW STEP. In a row of side-by-side text the smallest text lead is the row's step, and
     the row's rules and folio marks sit on multiples of it from the shared first baseline.
     gridSnap.js measures it and writes it inline on each item of the row; declared and unset
     here (not a runtime token: consumers lint shared/src with their own lists), and unset on
     every item so a nested row never inherits an outer one's. Read it with a fallback:
     top: calc(-1 * var(--row-step, 24px)). */
  .wm-baselines > * { --row-step: initial; }
}

/* BASELINES ON THE LINE -- UNLAYERED, on purpose. type.css's .t-* classes are unlayered (they
   are its API, not a default), and an unlayered rule beats any layered one however specific:
   inside a layer, .wm-lines .t-ui lost to .t-ui and ui sat at 16.8 instead of 15. Out here,
   specificity decides, and .wm-lines X outranks X.

   Inside .wm-lines a text block's leading is --lh, a whole number of units, and its box
   starts on a line. That does not yet put its BASELINES there: a baseline sits the
   half-leading plus the ascent below the top of its line box, and that depends on the face.
   So each block is nudged down by however far that lands short of the next line -- under
   one unit, by `top`, which moves the glyphs and not the layout. The browser rounds ascent
   and descent to whole pixels at each size and floors an odd half-leading (lede: 27 - 20 = 7,
   so 3 above, not 3.5), and so does this. Cal Sans's hhea is 1800 / -490 on 2000; a page on
   another face sets --font-asc / --font-desc from its own (or typo, if USE_TYPO_METRICS).
   A page sets --lh wherever it sets a leading; nothing here touches margins. */
.wm-lines {
  --font-asc: 0.9; --font-desc: 0.245;
  /* the roles' own leadings (type.css rounds each to --bl); the px are what they come to at
     the 16px root, and what a page without type.css gets */
  --lead-micro: var(--type-micro-lead, 12px); --lead-ui: var(--type-ui-lead, 15px);
  --lead-body: var(--type-body-lead, 24px); --lead-lede: var(--type-lede-lead, 27px);
  --lead-title: var(--type-title-lead, 30px); --lead-display: var(--type-display-lead, 51px);
  --lh: var(--lead-body);
}
/* :where, so any rule that sets --lh outranks it, whatever selector it is written with.
   A STAGE IS NOT REACHED. The trailing :not(:where(...)) leaves out a [data-nosnap] subtree
   (the element itself, or anything inside it): a demo with its own type keeps its own leading,
   position and top, and a shell no longer hands them back with a `revert-layer` rule. The
   :where inside the :not is what keeps the specificity story true -- a :not() counts its
   argument, so a bare :not([data-nosnap] *) would lift this from (0,1,0) to (0,2,0) and a
   page's own `.card p` would stop outranking it. */
.wm-lines :where(p, li, h1, h2, h3, h4, h5, figcaption, .t-display, .t-title, .t-lede, .t-ui, .t-label, .t-micro, [data-line]):not(:where([data-nosnap], [data-nosnap] *)) {
  line-height: var(--lh);
  position: relative;
  /* unset until a builder or gridSnap.js writes it ON the element (inline wins); declared
     here, not listed as a runtime token, so consumers' own token lists don't fail on it */
  --snap: initial;
  --asc: round(calc(var(--font-asc) * 1em), 1px);
  --desc: round(calc(var(--font-desc) * 1em), 1px);
  top: calc(mod(calc(var(--bl) - mod(calc(round(down, (var(--lh) - var(--asc) - var(--desc)) / 2, 1px) + var(--asc)), var(--bl))), var(--bl)) + var(--snap, 0px));
}
/* --snap: what CSS cannot know. The nudge above is exact for a block whose BOX starts on a
   line; a block below an image, an embed or a 1px rule starts wherever those end. A builder
   (or src/gridSnap.js, after layout) measures the rendered baseline and sets --snap to the
   remainder -- under one unit, glyphs only, never the layout. */
/* The roles' --lh, out of a stage by the same :not(:where()) -- (0,2,0) as before, not more. */
.wm-lines .t-lede:not(:where([data-nosnap], [data-nosnap] *)) { --lh: var(--lead-lede); }
.wm-lines .t-title:not(:where([data-nosnap], [data-nosnap] *)) { --lh: var(--lead-title); }
.wm-lines :is(.t-ui, .t-label):not(:where([data-nosnap], [data-nosnap] *)) { --lh: var(--lead-ui); }
.wm-lines .t-micro:not(:where([data-nosnap], [data-nosnap] *)) { --lh: var(--lead-micro); }
.wm-lines .t-display:not(:where([data-nosnap], [data-nosnap] *)) { --lh: var(--lead-display); }
