/* ============================================================================
   Base layer. The craft rules from the cheap/expensive delta, encoded so a
   component cannot get them wrong. See design/research/house-standard.md §2.
   ========================================================================= */

@import url('./mobile.css');

@layer base {
  *, *::before, *::after { box-sizing: border-box }

  html {
    -webkit-text-size-adjust: 100%;
    /* Smooth scroll only when the user has not asked otherwise. The token
       layer already handles reduce, this is belt and braces for anchors. */
    scroll-behavior: smooth;
  }
  @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto } }

  body {
    margin: 0;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--t-body);
    line-height: var(--lh-body);
    letter-spacing: var(--tr-body);
    font-weight: var(--w-regular);

    /* Type rendering. -moz-osx-font-smoothing only; NEVER
       -webkit-font-smoothing: antialiased, which thins strokes and is a
       common self-inflicted "why does my type look weak" bug. */
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    font-kerning: normal;
    font-variant-numeric: tabular-nums;   /* specs and dimensions must align */
  }

  /* --- headings ---------------------------------------------------------
     text-wrap: balance kills orphans and single-word last lines. This is one
     of the details that separates "designed" from "generated". */
  h1, h2, h3, h4 {
    margin: 0;
    font-weight: var(--w-medium);
    font-family: var(--font-display, var(--font-body));
    text-wrap: balance;
    /* Trim to cap height so a heading optically aligns with what sits beside
       it, rather than carrying the font's built-in leading as phantom space. */
    text-box: trim-both cap alphabetic;
  }
  h1 { font-size: var(--t-h1); line-height: var(--lh-h1); letter-spacing: var(--tr-h1) }
  h2 { font-size: var(--t-h2); line-height: var(--lh-h2); letter-spacing: var(--tr-h2) }
  h3 { font-size: var(--t-h3); line-height: var(--lh-h3); letter-spacing: var(--tr-h3) }
  h4 { font-size: var(--t-h4); line-height: var(--lh-h4); letter-spacing: var(--tr-h4) }

  p  { margin: 0; text-wrap: pretty; max-width: var(--max-text) }

  /* A KICKER SITTING ON THE HEADLINE.

     Headings carry margin:0 and the small uppercase label above them carries
     none either, so an eyebrow like "A GIFT OUT OF ARMENIA" landed flush on the
     cap-height of the h1 below it -- measured gap 0px on every site. These
     labels are letterspaced and set in the mono face, so the collision reads as
     a typographic mistake rather than as tight styling.

     One rule rather than a margin on the label itself, because the label is
     also used on its own where a trailing margin would be wrong. */
  :is(.mono, .label, .kicker, .eyebrow) + :is(h1, h2, h3) { margin-top: var(--sp-3) }

  /* Uppercase always gets a tracking bump. Without it, caps look jammed. */
  .eyebrow {
    font-size: var(--t-micro);
    line-height: var(--lh-micro);
    letter-spacing: var(--tr-micro);
    font-weight: var(--w-medium);
    text-transform: uppercase;
    color: var(--ink-3);
  }

  /* --- media -------------------------------------------------------------
     Every image carries its own aspect-ratio so nothing shifts on load.
     Layout shift is the single most visible cheap-site tell there is. */
  img, video, svg { display: block; max-width: 100%; height: auto }
  img[width][height] { aspect-ratio: attr(width) / attr(height) }

  /* --- focus -------------------------------------------------------------
     Visible for keyboard users, absent for mouse users. Never remove it
     outright. Offset so the ring is not clipped by the element's own edge. */
  :focus { outline: none }
  :focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 3px;
    border-radius: 1px;
  }

  /* --- hairlines ---------------------------------------------------------
     A "1px" border on a 2x screen is 2 device pixels and reads heavy. Use a
     sub-pixel value so it renders as a true hairline where the display allows. */
  .rule { border: 0; border-top: 1px solid var(--rule) }
  @media (min-resolution: 2dppx) { .rule { border-top-width: 0.5px } }

  /* break-word so a URL used as its own link text wraps instead of pushing the
     document sideways. One facebook.com/groups/... link on the club page ran a
     360px viewport 4px over. break-word, not anywhere: it only breaks when the
     word would otherwise overflow, so ordinary prose is untouched. */
  a { color: inherit; text-underline-offset: 0.18em; text-decoration-thickness: 0.06em;
      overflow-wrap: break-word }

  /* Long catalogue lists render lazily. Cheap win, large effect. */
  .deferred { content-visibility: auto; contain-intrinsic-size: auto 600px }
}

@layer utilities {
  .grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
    column-gap: var(--gutter);
    max-width: var(--max-content);
    margin-inline: auto;
    padding-inline: var(--margin);
  }
  .section   { padding-block: var(--section-y) }
  .section-major { padding-block: var(--section-y-major) }
  .measure   { max-width: var(--max-text) }
  .visually-hidden {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }

  /* Scroll reveal in pure CSS. No JS, no scroll listener, no jank.
     Gated on support and on reduced-motion via the token layer. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reveal {
        animation: reveal-in linear both;
        animation-timeline: view();
        animation-range: entry 10% cover 32%;
      }
      @keyframes reveal-in {
        from { opacity: 0; transform: translateY(20px) }
        to   { opacity: 1; transform: none }
      }
    }
  }
}

@layer components {
  /* ------------------------------------------------------------------
     Data tables: the header stays readable and the table stays on the page.

     Two bugs lived here, and they were the same mistake made twice: every
     table wrapper was turned into a horizontal scroll container, for the sake
     of the handful that need one.

     BUG ONE, the header sitting on top of the first row. The note that used to
     be here claimed overflow-y:clip keeps the wrapper out of the scroll
     container business. It does not. The spec is explicit: when one axis is
     auto or scroll, clip on the other axis COMPUTES TO HIDDEN, and hidden is
     still a scroll container. Chrome agrees, getComputedStyle reported
     overflow-y:hidden on all 50 hub pages that carry a table. So sticky
     resolved against the wrapper rather than the viewport, and because the
     wrapper never scrolls vertically, top:3.5rem stopped meaning "pin under
     the masthead" and started meaning "sit 3.5rem lower than you belong".
     Measured on the shoaling page: the header sat 21px into the first row,
     opaque, sliced the tops off the glyphs, and stayed there at every scroll
     position.

     There is no way to have both. A viewport-sticky header needs the viewport
     to be the nearest scrollport, and a horizontal scroller takes that job
     away. So the choice is made per table rather than for all of them, and
     the default goes to the common case.

     BUG TWO, the table hanging off the right of the window. The wrapper asked
     for min(100vw - 2*margin, 1440px), which at a 1440 window is 1312, inside
     a grid track of 1072. justify-self:center was supposed to spread that
     240px evenly over both sides. It does not. An item bigger than its
     alignment container is clamped back to start, so the whole 240px went
     right and the document measured 1496 against a 1440 window. All 50 pages
     did it, every one by the same 56px.

     The table itself was innocent. max-width:100% was being honoured; the box
     that overflowed was the wrapper, and it overflowed because we asked it to.
     ------------------------------------------------------------------ */

  /* THE DEFAULT, above phone width: a table belongs to the page, not to a
     window onto the page. Of 183 tables across the sites 140 have five columns
     or fewer, and at a 1440 window exactly one of them was wider than its
     wrapper. Those tables want to wrap and be read, so the wrapper stays an
     ordinary block. Nothing above the header is a scroll container any more,
     which is what gives the sticky rule at the bottom of this file its
     scrollport back.

     .tw:has(table) is 0,1,1 and outranks the page-local `.tw` at 0,1,0, which
     is how the 69 pages that set their own overflow-x are overruled without
     editing any of them. justify-self goes back to stretch because a grid item
     with width:auto and justify-self:center shrinks to fit instead. */
  .tw:has(table) {
    width: auto;
    max-width: none;
    justify-self: stretch;
    margin-inline: 0;
    overflow: visible;
  }
  /* min-width is reset because 68 pages set min-width:600px on their tables to
     force the old sideways scroll. With no scroller under it that rule is just
     a 600px page overflow on a phone. */
  .tw:has(table) > table { width: 100%; min-width: 0; max-width: 100% }

  /* A prose column will take every pixel it is offered. At 1440 the "what it
     came from" column on the shoaling table ran to 1066px, roughly three times
     a comfortable measure, and that is why the table read as falling off the
     screen even where it technically fitted. Capping the cell caps the column,
     and the short columns beside it keep their own width. */
  .tw:has(table) td, .tw:has(table) th { max-width: 58ch; overflow-wrap: break-word }

  /* The page-local stylesheets bundle two unrelated things into `.num`:
     tabular figures, which is the point of the class, and white-space:nowrap,
     which was a convenience for cells reading "4 to 10 m". Tabular figures
     stay. nowrap has to go, because it puts an UNBOUNDED FLOOR under the table
     width, and 44 of the 2610 .num cells on these sites hold a whole sentence.
     One of them, on the Sevan page, wanted 766px on one line and dragged the
     document 208px wider than the window.

     Losing it costs less than it sounds. Auto table layout hands a column its
     max-content width whenever the table has room, so "4 to 10 m" still sits
     on one line everywhere it fits. It now breaks only under real pressure,
     which is the one moment when breaking is the right answer. */
  .tw:has(table) .num { white-space: normal }

  /* Same unbounded floor, different class. 13 pages set `thead th` to nowrap,
     and "What you are doing while she does it" is 243px that the table can
     never go under. That one header, times four columns, is why the decision
     rights table on the children page stuck 24px out of a 768 window. A
     column heading that runs to two lines is normal and costs nothing. */
  .tw:has(table) thead th { white-space: normal }

  /* THE OPT-IN: tables that genuinely want to scroll sideways. .tw-wide is the
     class the psychology pages already write by hand, so that is the name. The
     :has() clause picks up the rest by shape rather than by annotation, since
     43 tables would otherwise need a hand edit each: a row with a sixth cell
     is a table no window is going to lay out honestly. The eleven-column
     difficulty tables and the nine-column shops and reading-score tables are
     what this exists for. */
  .tw-wide:has(table),
  .tw:has(table tr > :nth-child(6)) {
    overflow-x: auto;
    overflow-y: hidden;        /* what clip computes to here anyway, said out loud */
    overscroll-behavior-x: contain;
    /* Bleed into the page gutters and no further. The old min(100vw - ...)
       measured the viewport including its scrollbar and then overflowed by the
       difference; a negative margin of one gutter per side is the same gesture
       bounded by the grid, so it cannot exceed the window. */
    margin-inline: calc(-1 * var(--margin));
  }
  .tw-wide:has(table) > table,
  .tw:has(table tr > :nth-child(6)) > table {
    width: max-content;
    min-width: 100%;
    max-width: none;
  }
  /* The measure cap stays on inside a scroller, and it matters more here than
     it does outside one. Numeric cells are nowhere near 58ch so the cap never
     touches them, but an uncapped prose column takes its full max-content and
     turns a nudge into a drag: on a phone the shoaling table measured 2540px
     of sideways scroll for three columns before the cap went back on.
     nowrap costs nothing here though, because there is somewhere to put it,
     and a figure that breaks mid-value is worse than a short drag. */
  .tw-wide:has(table) .num, .tw-wide:has(table) thead th,
  .tw:has(table tr > :nth-child(6)) .num,
  .tw:has(table tr > :nth-child(6)) thead th { white-space: nowrap }

  /* The trade, stated plainly: inside a scroller the nearest scrollport is the
     wrapper, so a sticky header cannot reach the masthead and would hover over
     row one instead. These headers stay in the flow. */
  .tw-wide:has(table) thead th,
  .tw:has(table tr > :nth-child(6)) thead th { position: static }

  /* NARROWER WINDOWS, where the same argument comes out the other way. Six
     columns is the right threshold at 1440 and nowhere near it further down,
     so it steps with the measure the page actually has. Both numbers were
     found by sweeping all 50 hub pages and reading document.scrollWidth, not
     chosen for looking tidy.

     Tablet, 672px of table at a 768 window: the 21-species mis-sold table on
     the difficulty page has five columns and wanted 892 of it, so five is
     where the line goes. */
  @media (max-width: 1023px) {
    .tw:has(table tr > :nth-child(5)) {
      overflow-x: auto;
      overflow-y: hidden;
      overscroll-behavior-x: contain;
      margin-inline: calc(-1 * var(--margin));
    }
    .tw:has(table tr > :nth-child(5)) > table {
      width: max-content;
      min-width: 100%;
      max-width: none;
    }
    .tw:has(table tr > :nth-child(5)) .num,
    .tw:has(table tr > :nth-child(5)) thead th { white-space: nowrap }
    .tw:has(table tr > :nth-child(5)) thead th { position: static }
  }

  /* Phone, 342px of table at a 390 window: letting everything wrap down here
     pushed 36 of the 50 pages past the window, by up to 574px, and a page
     that slides sideways is the worst of the three outcomes. Three columns is
     as many as 342px can hold. This is also what these pages already did at
     every width before the fix, so on a phone the only thing that changes is
     that the header stops sitting on row one. */
  @media (max-width: 640px) {
    /* Tighter down here: 58ch is 464px against 342px of window, so it would
       never bite, and the scroll it allows would be most of a screen wide. */
    .tw:has(table) td, .tw:has(table) th { max-width: 32ch }

    .tw:has(table tr > :nth-child(3)) {
      overflow-x: auto;
      overflow-y: hidden;
      overscroll-behavior-x: contain;
      margin-inline: calc(-1 * var(--margin));
    }
    .tw:has(table tr > :nth-child(3)) > table {
      width: max-content;
      min-width: 100%;
      max-width: none;
    }
    .tw:has(table tr > :nth-child(3)) .num,
    .tw:has(table tr > :nth-child(3)) thead th { white-space: nowrap }
    .tw:has(table tr > :nth-child(3)) thead th { position: static }
  }

  /* Sticky headers, which now actually stick, for every table that did not
     opt into a scroller above. */
  .tw thead th {
    position: sticky;
    top: var(--sticky-top, 5rem);
    z-index: 4;                        /* under the masthead's 10 */
    background: var(--surface);
    box-shadow: inset 0 -1px 0 var(--rule);
  }
  .tw thead tr + tr th { top: calc(var(--sticky-top, 5rem) + 2.25rem) }

  /* 5rem is the masthead, measured rather than guessed: .nav is 80px at
     desktop and 76px on a phone. The old 3.5rem predates sticky ever firing,
     so it had never been checked against anything, and it would have parked
     the header 24px up behind a nav that sits at z-index 10. */
  @media (max-width: 1023px) {
    .tw thead th { top: var(--sticky-top-sm, 4.75rem) }
    .tw thead tr + tr th { top: calc(var(--sticky-top-sm, 4.75rem) + 2.25rem) }
  }
}
