/* ==========================================================================
   rl-rhythm.css  v1.10 (2026-09-30)
   Universal vertical rhythm: every container is the same distance from its
   content at the top as at the bottom, measured to the INK, on any site.

   Why this file exists. The same defect reaches the owner on every project:
   a section or card reads looser at one edge than the other. Three causes,
   all structural, none of them a one-off:
     1. Trailing margins. p, ul, h* carry margin-bottom; any container that
        does not zero its last child's margin gets a fatter bottom.
     2. Split edge ownership. The top gap belongs to the container's padding,
        the bottom gap to the last child's margin plus the next block's
        margin-top. Symmetry is nobody's property.
     3. Boxes versus ink. CSS pads to the line box; the eye measures to the
        cap top and the baseline. A 40px heading's cap sits ~8px inside its
        line box, a 16px paragraph's baseline ~4px inside its box, so equal
        padding reads uneven and a card below reads tighter than a heading
        above.
   Every earlier fix was an enumerated selector list (named cards, named
   wrappers), so each new wrapper regenerated the defect. This file is
   selector-generic: it keys on structure (section, article, column,
   container) and on class-name fragments every bounded block already
   carries (-card, -body, -box, -panel, -band, -txt, -content ...).

   How to use.
     - Load it AFTER the site stylesheet so ties go to the rhythm.
     - Set the variables in the site CSS :root (fallbacks are Inter / 56px).
       `scripts/check-rule-spacing.js <url> --fonts` prints the exact values
       for every font and weight a page uses.
     - `scripts/check-rule-spacing.js <url> --insets --sections` then proves
       every container's top and bottom inset within 2px.
     - A container that must keep its own child margins gets
       class="rl-rhythm-off" (nothing in here touches its children).
   Needs the `lh` unit (Chrome 109+, Safari 16.4+, Firefox 120+); older
   browsers drop the ink rules and keep the box rules, which is still even.

   Specificity contract (v1.1 broke on this; v1.4 lost a tie on load order
   because a blog template's inline <style> came after this file; keep it exact):
     layer 1  section padding          (0,0,0)  a default; any site rule wins
     layer 2  zero first/last margins  (0,2,0)  beats element rules AND a
                                                single-class component margin
                                                (`.x-section { margin-bottom }`)
                                                whatever the load order
     layer 3  replaced-element edges   (0,1,1)
     layer 4  ink pull-in              (0,2,1)  beats layer 2 and the
                                                enumerated `.x-card > :last-child`
                                                lists in component CSS
   Every qualifier that is not meant to add weight is wrapped in :where().
   Only `:not(.rl-rhythm-off, nav *)` on layers 2 and 4 and the subject
   pseudo-classes carry weight. A deliberate exception is `.rl-rhythm-off`
   on the container, never a heavier selector.

   Site variables (set in the site's :root; fallbacks apply when unset):
     --rl-rhythm            section padding, measured to the ink. 56px is the
                            Risingline standard; legacy sites keep their own
                            (Madroña: 48px).
     --rl-ink-top-head, --rl-ink-bottom-head, --rl-ink-top-body, --rl-ink-bottom-body
                            ink constants in em, per font, from the font's
                            vertical metrics (ascent a, descent d, cap height c,
                            as fractions of 1em):
                              top    = c + (d - a) / 2   cap top sits (0.5lh - top) below the line-box top
                              bottom = (a - d) / 2       baseline sits (0.5lh - bottom) above the line-box bottom
                            Inter: a .969 d .242 c .727 -> .363 / .363 (the one
                            font where both are equal). Vollkorn .424 / .255.
                            Archivo .351 / .335.
   No :root block here on purpose: a declaration in this file would override
   the site's values when it loads last.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Sections carry the rhythm (default, zero specificity). Bands (full-bleed
      colored blocks) inside a section own their own edges with the same
      inset; wrap them in a section that carries .band so the section adds
      nothing.
   -------------------------------------------------------------------------- */
:where(main > section, main > article, .rl-band):where(:not(.rl-rhythm-off, .band)) {
  padding-top: var(--rl-rhythm, 56px);
  padding-bottom: var(--rl-rhythm, 56px);
}
main > section.band {
  padding-top: 0;
  padding-bottom: 0;
}

/* --------------------------------------------------------------------------
   2. Containers own BOTH edges: no child margin against a padding edge.
      The container list is structural plus class-fragment matching so a new
      wrapper is covered the day it is written.
   -------------------------------------------------------------------------- */
:where(
  section, article, aside, figure, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-item"], [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):not(.rl-rhythm-off, nav *) > :first-child:where(:not(.row)) {
  margin-top: 0;
}
:where(
  section, article, aside, figure, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-item"], [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):not(.rl-rhythm-off, nav *) > :last-child {
  margin-bottom: 0;
}
/* A grid row keeps its negative gutter compensation (Bootstrap 5); on
   Bootstrap 3 the variable is undefined and this is 0. */
:where(section, article, .container, .container-fluid, [class*="col-"]) > .row:first-child {
  margin-top: calc(-1 * var(--bs-gutter-y, 0px));
}

/* --------------------------------------------------------------------------
   3. Replaced elements at an edge: an inline image, iframe or video leaves
      descender space under itself. Keep it inline (centering still works),
      drop the gap. A paragraph that only wraps a button is a box too: the
      button sits at the top of its line box so the line box is exactly the
      button's height.
   -------------------------------------------------------------------------- */
:where(section, article, figure, .container, [class*="col-"], [class*="-body"], [class*="-content"], [class*="-card"])
  > :is(img, svg, iframe, video, canvas, embed, object):last-child {
  vertical-align: bottom;
}
:where(p, div):where(:has(> :is(button, [class*="btn"]):only-child)) > :is(button, [class*="btn"]) {
  display: inline-block; /* an inline link styled as a button lets its padding spill past the line box */
  vertical-align: top;
}

/* --------------------------------------------------------------------------
   4. Optical edges: the first and last TEXT child pulls in so its ink, not
      its line box, sits at the container's padding. Only the innermost
      direct text child moves; wrappers in between carry zero margin (layer
      2), so the pull-in reaches the section edge through .container > .row >
      .col chains as well as through a padded card. A paragraph that only
      wraps a button or image is a box, not text, and is left alone. The
      pull-in is only right against a PADDING edge: the container must be
      first (or last) in its parent, a side-by-side column, or a padded
      block. A bare wrapper that follows a figure or a paragraph keeps its
      sibling's margin; pulling there would eat the gap under a photo. A div
      that holds only inline content (a meta line, a back link) is text; an
      empty div (a scroll anchor) is nothing and must not move. A list that
      is a control (tabs, pills, pagination, menus, button groups) is a row
      of boxes, not text, and gets no pull-in.
   -------------------------------------------------------------------------- */
:where(
  section, article, aside, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):where(:first-child, [class*="col-"], section, main > article, .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane, blockquote, [class*="-card"], [class*="-box"], [class*="-panel"], [class*="-band"], [class*="-txt"], [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"], [class*="-item"]):not(.rl-rhythm-off, nav *) > :is(h1, h2, h3, h4, h5, h6):first-child {
  margin-top: calc(var(--rl-ink-top-head, 0.363em) - 0.5lh);
}
:where(
  section, article, aside, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):where(:first-child, [class*="col-"], section, main > article, .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane, blockquote, [class*="-card"], [class*="-box"], [class*="-panel"], [class*="-band"], [class*="-txt"], [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"], [class*="-item"]):not(.rl-rhythm-off, nav *) > :is(p, dd, dt, address, figcaption, label, small, div:where(:not(:empty):not(:has(> :is(p, div, ul, ol, dl, h1, h2, h3, h4, h5, h6, figure, table, blockquote, section, article, nav, form, img, iframe, video, button, [class*="btn"]))))):where(:not(:has(> :is(img, svg, iframe, video, button, [class*="btn"]):only-child))):first-child,
:where(
  section, article, aside, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):where(:first-child, [class*="col-"], section, main > article, .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane, blockquote, [class*="-card"], [class*="-box"], [class*="-panel"], [class*="-band"], [class*="-txt"], [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"], [class*="-item"]):not(.rl-rhythm-off, nav *) > :is(ul, ol):where(:not(.nav, .nav-pills, .nav-tabs, .pagination, .list-group, .btn-group, .breadcrumb, [role="tablist"], [role="navigation"], [role="menu"], [role="menubar"], [class*="-nav"], [class*="-menu"], [class*="-tabs"], [class*="-pills"], [class*="-buttons"])):first-child > :where(li:first-child) {
  margin-top: calc(var(--rl-ink-top-body, 0.363em) - 0.5lh);
}
:where(
  section, article, aside, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):where(:last-child, [class*="col-"], section, main > article, .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane, blockquote, [class*="-card"], [class*="-box"], [class*="-panel"], [class*="-band"], [class*="-txt"], [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"], [class*="-item"]):not(.rl-rhythm-off, nav *) > :is(p, dd, dt, address, figcaption, label, small, div:where(:not(:empty):not(:has(> :is(p, div, ul, ol, dl, h1, h2, h3, h4, h5, h6, figure, table, blockquote, section, article, nav, form, img, iframe, video, button, [class*="btn"]))))):where(:not(:has(> :is(img, svg, iframe, video, button, [class*="btn"]):only-child))):last-child,
:where(
  section, article, aside, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):where(:last-child, [class*="col-"], section, main > article, .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane, blockquote, [class*="-card"], [class*="-box"], [class*="-panel"], [class*="-band"], [class*="-txt"], [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"], [class*="-item"]):not(.rl-rhythm-off, nav *) > :is(ul, ol):where(:not(.nav, .nav-pills, .nav-tabs, .pagination, .list-group, .btn-group, .breadcrumb, [role="tablist"], [role="navigation"], [role="menu"], [role="menubar"], [class*="-nav"], [class*="-menu"], [class*="-tabs"], [class*="-pills"], [class*="-buttons"])):last-child > :where(li:last-child) {
  margin-bottom: calc(var(--rl-ink-bottom-body, 0.363em) - 0.5lh);
}
:where(
  section, article, aside, blockquote, main > div,
  .container, .container-fluid, [class*="col-"],
  .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane,
  [class*="-card"], [class*="-body"], [class*="-box"], [class*="-panel"],
  [class*="-band"], [class*="-txt"], [class*="-text"], [class*="-content"],
  [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"],
  [class*="-block"], [class*="-section"], [class*="-sidebar"], [class*="-widget"], [class*="-intro"], [class*="-lede"], [class*="-summary"]
):where(:last-child, [class*="col-"], section, main > article, .card, .card-body, .panel, .panel-body, .well, .alert, .tab-pane, blockquote, [class*="-card"], [class*="-box"], [class*="-panel"], [class*="-band"], [class*="-txt"], [class*="-inset"], [class*="-callout"], [class*="-note"], [class*="-cell"], [class*="-item"]):not(.rl-rhythm-off, nav *) > :is(h1, h2, h3, h4, h5, h6):last-child {
  margin-bottom: calc(var(--rl-ink-bottom-head, 0.363em) - 0.5lh);
}

/* --------------------------------------------------------------------------
   5. List markers sit on the text edge (owner 2026-09-30, "recurring across
      all sites"). Browsers hang numbers and bullets 40px outside the content
      edge, so "A." floats left of the heading above it. Content lists (no
      class; styled lists carry one) put the marker inside the box flush with
      the text, and wrapped lines hang under the first word, not the marker.
   -------------------------------------------------------------------------- */
:where(main, article, section, .container) :is(ol, ul):where(:not([class])) {
  list-style-position: inside;
  padding-left: 0;
}
:where(main, article, section, .container) :is(ol, ul):where(:not([class])) > li {
  padding-left: 1.6em;
  text-indent: -1.6em;
}
:where(main, article, section, .container) :is(ol, ul):where(:not([class])) > li > * {
  text-indent: 0; /* text-indent inherits: an inline-block (icon, badge) inside the item would shift left onto the text */
}
:where(main, article, section, .container) :is(ol, ul):where(:not([class])) > li > :is(ol, ul) {
  margin-left: 1.6em; /* nested lists start under the parent's text */
}
:where(main, article, section, .container) :is(ol, ul):where(:not([class])) > li > :is(ol, ul) > li {
  text-indent: -1.6em;
}
