/* BrightKidz Hub — "The Rulebook"
 *
 * A printed board-game rulebook and its board. Flat inks on board stock, hard
 * offset shadows, die-cut corners. See DESIGN.md for the direction contract.
 *
 * No remote requests of any kind live in this file: no @import, no font CDN,
 * no url() pointing off-origin. A webfont request would carry a child's IP to
 * a third party and falsify the site's privacy claim.
 */

/* ---------------------------------------------------------------- tokens -- */

:root {
  /* Board stock. Warm, printed, low-glare — not cream paper, not white. */
  --stock:        #f4efe4;
  --stock-deep:   #e8dfcd;
  --board:        #211d18;   /* printed near-black; the box lid */
  --rule:         #211d18;

  /* Ink, read by every component. Bound per subject on the page root. */
  --ink:          #1f6f78;
  --ink-soft:     color-mix(in srgb, var(--ink) 14%, var(--stock));
  --ink-edge:     color-mix(in srgb, var(--ink) 62%, var(--board));

  /* Six subject inks — each owns whole regions, never a tint. */
  --ink-science:    #12676f;
  --ink-technology: #5b3fa8;
  --ink-nature:     #1d7533;  /* darkened: 4.44 failed AA */
  --ink-math:       #935824;  /* darkened: 4.28 failed AA */
  --ink-arts:       #b8324c;
  --ink-history:    #3a5794;

  /* Printed card, not soft UI. Hard offset, zero blur. */
  --shadow:       2px 2px 0 var(--rule);
  --shadow-lift:  4px 4px 0 var(--rule);
  --stroke:       3px;
  --die-cut:      4px;

  --measure:      66ch;
  --gap:          clamp(1rem, 3vw, 1.75rem);

  --font-display: "Arial Black", "Helvetica Neue", Impact, system-ui, sans-serif;
  --font-read:    ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono:    ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
}

[data-subject="Science"]              { --ink: var(--ink-science); }
[data-subject="Technology"]           { --ink: var(--ink-technology); }
[data-subject="Nature & Environment"] { --ink: var(--ink-nature); }
[data-subject="Math & Logic"]         { --ink: var(--ink-math); }
[data-subject="Arts & Creativity"]    { --ink: var(--ink-arts); }
[data-subject="History & Geography"]  { --ink: var(--ink-history); }

@media (prefers-contrast: more) {
  :root { --stock: #fffdf7; --board: #000; --rule: #000; }
}

/* ------------------------------------------------------------------ base -- */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--stock);
  color: var(--board);
  font: 19px/1.65 var(--font-read);
  -webkit-text-size-adjust: 100%;
}

/* Printed register marks in the margin — the world's own ornament. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, transparent 0 63px, color-mix(in srgb, var(--board) 3%, transparent) 63px 64px);
  z-index: 0;
}
body > * { position: relative; z-index: 1; }

main { margin-inline: auto; padding: 0 var(--gap) 4rem; }
/* The reading measure belongs to the rulebook SPREAD. A board is a board: it
   gets the full sheet, or the regions crush into a column. */
body[data-kind="article"] main { max-width: var(--measure); }
body[data-kind="listing"] main { max-width: 64rem; }
body[data-kind="listing"] .board { padding-inline: 0; }

h1, h2, h3 { font-family: var(--font-display); line-height: 1.1; letter-spacing: -0.01em; }
h1 { font-size: clamp(2rem, 6vw, 3rem); margin: 0 0 .4em; }
h2 { font-size: clamp(1.4rem, 3.6vw, 1.9rem); margin: 2.2em 0 .5em; }
h3 { font-size: 1.2rem; margin: 1.8em 0 .4em; }
p  { margin: 0 0 1.1em; }

a { color: var(--ink-edge); text-underline-offset: 3px; text-decoration-thickness: 2px; }
a:focus-visible, button:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

ul, ol { padding-left: 1.3em; }
li { margin: .45em 0; }

code { font-family: var(--font-mono); font-size: .92em; background: var(--ink-soft); padding: .1em .35em; border-radius: 3px; }
pre {
  font-family: var(--font-mono); font-size: .9rem; line-height: 1.5;
  background: var(--board); color: var(--stock);
  padding: 1rem 1.1rem; border-radius: var(--die-cut);
  overflow-x: auto; border: var(--stroke) solid var(--rule); box-shadow: var(--shadow);
}
pre code { background: none; padding: 0; color: inherit; }

/* --------------------------------------------------------------- masthead -- */

/* SAFE AREAS. On the web every env(safe-area-inset-*) is 0, so these max()
   calls resolve to exactly the values below and nothing changes. Inside the
   Capacitor shell they are not 0: without them the masthead sits under the
   status bar and the notch, and the footer under the home indicator, on every
   modern phone. Paired with viewport-fit=cover in the page shell -- without
   that meta value iOS reports the insets as 0 and this is silently inert. */
.masthead {
  border-bottom: var(--stroke) solid var(--rule);
  background: var(--stock);
  padding: max(.9rem, env(safe-area-inset-top)) max(var(--gap), env(safe-area-inset-right))
           .9rem max(var(--gap), env(safe-area-inset-left));
  display: flex; flex-wrap: wrap; gap: .75rem 1.25rem; align-items: center;
}
.wordmark {
  font-family: var(--font-display);
  font-size: 1.35rem; letter-spacing: -0.02em; text-transform: uppercase;
  color: var(--board); text-decoration: none;
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: 44px;   /* measured 36px; it is the "go home" target on every page */
}
.wordmark .pip {
  width: 1.1em; height: 1.1em; border-radius: 50%;
  background: var(--ink-math); border: 2px solid var(--rule);
  display: inline-block;
}
.masthead nav { display: flex; flex-wrap: wrap; gap: .4rem; margin-left: auto; min-width: 0; }
/* ---------------------------------------------------------------- search -- */

.search-field { display: block; font-weight: 700; margin: 0 0 .8rem; }
.search-field input {
  display: block; width: 100%; margin-top: .4rem;
  font: inherit; font-size: 1.15rem;
  min-height: 48px; padding: .5rem .7rem;
  background: var(--stock); color: var(--board);
  border: var(--stroke) solid var(--rule); border-radius: var(--die-cut);
  box-shadow: var(--shadow);
}
/* A count, always. Silence after typing reads as "broken" to a child, and the
   empty case says what to DO rather than only that nothing was found. */
.search-status { margin: 0 0 1.2rem; font-weight: 700; }
.card-subject { margin: .5rem 0 0; font-size: .8rem; font-weight: 700; color: var(--ink); }
.nav-find { background: var(--stock) !important; }

/* The region header on a category page: the board tile, entered. Same ink
   block, same motif, same count, so following a tile reads as stepping into
   the region rather than leaving the board for a plain list. */
.region-head {
  display: flex; align-items: center; gap: 1rem;
  background: var(--ink); color: var(--stock);
  border: var(--stroke) solid var(--rule); border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  padding: 1.1rem 1.3rem; margin: 0 0 1.6rem;
}
.region-head .motif { width: 3.2rem; height: 3.2rem; flex: none; }
/* MEASURED: without min-width the text block refused to shrink below its own
   content and "Nature & Environment" pushed 19px off a 320px screen. A flex
   child's default min-width is auto, not 0 -- the single most common cause of
   overflow in a flex row. */
.region-head > div { min-width: 0; }
.region-head h1 { margin: 0; color: var(--stock); overflow-wrap: break-word; }
.region-head .count { margin: .2rem 0 0; font-size: .9rem; }

@media (max-width: 24rem) {
  .region-head { gap: .7rem; padding: .9rem 1rem; }
  .region-head .motif { width: 2.2rem; height: 2.2rem; }
}

/* Each card carries its subject's rule, so a listing is attributable at a
   glance instead of being an undifferentiated grid -- the category rut the
   design contract names as a prohibition. */
.article-list > li { border-top: var(--stroke) solid var(--ink); }

/* Was an inline style in build.py -- in the repo whose first stated lesson is
   that its own no-inline-style rule went unenforced and so unfollowed. */
.flush { margin-top: 0; }

/* Seven links repeat above every page. Visible only on focus, so it costs a
   keyboard user one tab and everyone else nothing; it is a printed tab in the
   world's own idiom when it does appear. */
.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--board); color: var(--stock);
  padding: .7rem 1rem; border-radius: 0 0 var(--die-cut) 0;
  font-weight: 700; text-decoration: none; z-index: 10;
}
.skip-link:focus { left: 0; }

/* MEASURED at 38px tall desktop, 34px at phone widths -- under the 44px
   fingertip floor DESIGN.md sets for the whole site, on the six pills that
   are the primary navigation of every page. The widgets honoured their 48;
   the chrome did not honour its 44. Height is set outright rather than
   guessed at through padding, so it holds at any font size. */
.masthead nav .nav-short { display: none; }
.masthead nav a {
  font-size: .82rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  text-decoration: none; color: var(--board);
  display: inline-flex; align-items: center; min-height: 44px;
  padding: .4rem .7rem; border: 2px solid var(--rule); border-radius: var(--die-cut);
  background: var(--stock-deep);
}
.masthead nav a:hover { background: var(--ink); color: var(--stock); }

/* Which subject you are looking at, on every page that has one. Six identical
   pills gave a child no "you are here" once they left the board. */
.masthead nav a[aria-current="page"] {
  background: var(--ink); color: var(--stock); border-color: var(--ink);
  box-shadow: var(--shadow);
}

/* ------------------------------------------------------------- the board -- */

/* THE LEDE SITS BELOW THE BOARD, at every width. DESIGN.md's FIRST VIEWPORT
   rule is "no hero paragraph, no marketing sentence -- a board is understood
   by looking", and the page shipped a sentence above the board anyway. It is
   also the wrong audience above the fold: "nothing is collected" is a sentence
   for a PARENT, and the child is here to choose a subject.
   MEASURED: moving it takes an iPad landscape from 3 of 6 tiles above the fold
   to all 6, and it is the same fix the phone layout needed. */
.board {
  max-width: 62rem; margin: clamp(1.5rem, 5vw, 3rem) auto; padding: 0 var(--gap);
  display: flex; flex-direction: column;
}
.board-title { order: 1; }
.regions     { order: 2; }
.board-lede  { order: 3; }
.board-title {
  font-family: var(--font-display); text-transform: uppercase;
  font-size: clamp(1.6rem, 5vw, 2.6rem); margin: 0 0 .15em;
}
.board-lede { max-width: 46ch; margin: 1.6rem 0 0; font-size: 1.05rem; }

/* grid-auto-rows: 1fr makes EVERY row the height of the tallest, so all six
   tiles are identical. Without it each row sized to its own content and the
   board came out in two or three different tile heights -- fine for a list,
   wrong for a board, where the regularity IS the thing being communicated. */
.regions { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; gap: var(--gap); list-style: none; margin: 0; padding: 0; }
/* THE COLOURED CARD MUST FILL ITS CELL.
   MEASURED on a 390px phone: the two <li> in a row both stretched to 107.125px
   (grid does that correctly), but the <a> inside one of them was 91.9px --
   content-sized. So the visible cards in a single row had different heights and
   the board looked ragged, which is exactly what a board must not look like.
   display:flex on the item makes the link a flex child, and the default
   align-items:stretch then gives it the cell's full height.

   margin:0 because the base `li { margin: .45em 0 }` was adding 8.55px above
   and below every tile ON TOP of the grid gap -- spacing the grid was already
   responsible for, and vertical space the board could not afford on a phone. */
.region { display: flex; margin: 0; }
.region, .region a { min-width: 0; }
.region a { flex: 1; }
.region .name { overflow-wrap: anywhere; }
/* Two columns only below 46rem. It was 52rem (832px), which handed an iPad
   in PORTRAIT (768px) the two-column phone shape and 342px-wide tiles -- a lot
   of screen for four visible regions. A tablet has room for the real board. */
@media (max-width: 46rem) { .regions { grid-template-columns: repeat(2, 1fr); } }




.region a {
  display: flex; flex-direction: column; gap: .5rem;
  min-height: 12.5rem; padding: 1.1rem;
  background: var(--ink); color: var(--stock);
  border: var(--stroke) solid var(--rule); border-radius: var(--die-cut);
  box-shadow: var(--shadow-lift); text-decoration: none;
  transition: transform .12s steps(2), box-shadow .12s steps(2);
}
.region a:hover, .region a:focus-visible { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--rule); }

/* ── MAKING THE BOARD LIVELIER, WITHOUT LEAVING THE WORLD ──────────────────
   The board read as flat and a bit solemn: six plain rectangles, a small
   line-art icon, a name. The fix is not to bolt on "fun" -- no gradients, no
   bounce, no mascot, all of which DESIGN.md prohibits and none of which a
   12-year-old reads as anything but babyish. It is to lean HARDER into what
   this world already is. A real board game is not solemn: it has numbered
   spaces, big printed pictures, and a token telling you where you are.

   Three moves, all of them devices DESIGN.md already names as native:
   a numbered space, a bigger illustration set at a printed angle, and a
   die-cut label for the count. */

/* The space number. CSS counters, so the markup is untouched and the numbers
   cannot drift out of order when a subject is added or removed. */
.regions { counter-reset: space; }
.region { counter-increment: space; }
.region a { position: relative; overflow: hidden; }
.region a::before {
  content: counter(space);
  position: absolute; top: .55rem; left: .6rem; z-index: 2;
  width: 1.6rem; height: 1.6rem;
  display: grid; place-items: center;
  font-family: var(--font-display); font-size: .95rem;
  color: var(--ink); background: var(--stock);
  border: 2px solid var(--rule); border-radius: 50%;
}

/* The illustration, printed larger and at an angle, bleeding off the corner
   the way art sits on a real board space rather than floating as an icon. */
.region .motif {
  /* Sized by HEIGHT, not width. At 72% width the motif scaled with the column,
     so on a 1280px laptop the tiles grew from 203px to 310px tall and the board
     fell from 6 visible regions to 3 -- a liveliness change that cost the thing
     liveliness was meant to serve. Height is the dimension the fold cares
     about, so height is the dimension to fix. */
  height: 5.5rem; width: auto; align-self: flex-end;
  margin: -.3rem -1.2rem .1rem 0;
  transform: rotate(-7deg);
}

.region .name { font-family: var(--font-display); text-transform: uppercase; font-size: 1.15rem; line-height: 1.1; }
/* No opacity. MEASURED in a browser: at .85 the counts came to 4.42 / 4.40 /
   4.48 against nature, math and arts -- three of six under the 4.5 that
   12.8px text needs. The inks were deliberately darkened so all six pass
   opaque (see the palette above); a fade handed that margin straight back,
   on the first viewport, to readers the design says are on poor screens. */
/* The count as a printed label, not a caption: a die-cut chip in board stock,
   which also lifts it clear of the ink it used to sit on. */
.region .count {
  font-size: .78rem; font-weight: 700; margin-top: auto;
  align-self: flex-start;
  color: var(--ink); background: var(--stock);
  border: 2px solid var(--rule); border-radius: var(--die-cut);
  padding: .12rem .45rem;
}

/* ⚠ THIS BLOCK MUST STAY AFTER THE BASE .region RULES ABOVE.
   It used to sit ~45 lines EARLIER in the file, where it had the same
   specificity as the base rules and therefore lost the cascade to them -- so
   the entire phone layout for the board was DEAD CODE from the day it was
   written, and every tile rendered at the desktop 12.5rem (200px) height on a
   phone. MEASURED: a 360x640 Android showed 0 of 6 tiles above the fold.
   A media query does not add specificity. Order decides. */
@media (max-width: 30rem) {
  /* STAYS TWO COLUMNS. It used to collapse to one, which turns the board into
     a menu: six tall rows a child scrolls through, one choice at a time. The
     design's whole premise is six coloured regions understood by LOOKING, and
     a list is not that. Two columns of compact tiles keeps the shape of a
     board and puts every subject on one screen.

     MEASURED before: 0 of 6 tiles above the fold on a 360x640 Android. */
  .regions { gap: .6rem; }
  .region a {
    /* Top padding RESERVES the space number's corner. It used to centre the
       content vertically, and on a 390px screen the name ran straight under
       the disc -- "(1)ENCE", "(3)TURE & ENVIRONMENT". An absolutely positioned
       badge has to be paid for in padding by whatever flows beneath it. */
    min-height: 5.2rem; padding: 1.75rem .55rem .55rem; gap: .15rem;
    justify-content: flex-start;
  }
  .region a::before {
    width: 1.3rem; height: 1.3rem; font-size: .78rem;
    top: .4rem; left: .45rem;
  }
  /* The illustration is DROPPED on a phone, not shrunk. At this size the tile
     is ~86px tall and the bleeding, angled motif came out as a cropped sliver
     in the corner -- it read as a rendering glitch rather than as art. The
     space number, the ink and the name carry the tile perfectly well, and
     colour is still never the only signal because the name is right there.
     It returns above 30rem, where there is room for it to be a picture. */
  .region .motif { display: none; }
  .region .name { font-size: .85rem; line-height: 1.12; }
  /* The count is the first thing to go when the screen is this small: the
     NAME and the COLOUR carry the information, and a grid row is as tall as
     its tallest tile, so one wrapped count line costs every tile. Kept at
     360px and up, where it fits on one line and tells a child how much is in
     there. Colour is still never the only signal -- the name is right above. */
  .region .count { font-size: .68rem; margin-top: 0; }

  /* Getting the whole board above the fold is worth more than air at the top:
     a child who can see all six chooses; one who can see two scrolls. */
  .board { margin-top: .8rem; }
  .board-title { margin-bottom: .35em; }

  /* The lede goes BELOW the board. It is a sentence for a parent ("nothing is
     collected"), not a thing a child reads before choosing, and DESIGN.md's
     own first-viewport rule says no marketing sentence above the board. */
  .board { display: flex; flex-direction: column; }
  .board-title { order: 1; font-size: 1.35rem; }
  .regions      { order: 2; }
  .board-lede   { order: 3; margin: 1.4rem 0 0; font-size: .95rem; }
}

/* The smallest phones still in use. One wrapped count line multiplies across
   every tile because grid rows equalise, and it is the least load-bearing
   thing on the tile. MEASURED at 360x640: this is what takes the board from
   2 tiles above the fold to all 6. */
@media (max-width: 23rem) {
  .region .count { display: none; }
  /* The display face is wide, and at this tile width "ENVIRONMENT" did not fit,
     so overflow-wrap:anywhere broke it as "ENVIRONMEN / T". A slightly smaller
     name fits the longest subject on one line, which is worth more than the
     extra point of size. */
  .region .name { font-size: .76rem; }
}

/* ------------------------------------------------- rulebook spread (article) -- */

.spread-head { border-bottom: var(--stroke) solid var(--rule); margin-bottom: 1.8rem; padding-bottom: 1.2rem; }
.subject-tag {
  display: inline-flex; align-items: center; gap: .45rem;
  font-family: var(--font-display); font-size: .78rem; text-transform: uppercase; letter-spacing: .06em;
  background: var(--ink); color: var(--stock);
  padding: .3rem .6rem; border: 2px solid var(--rule); border-radius: var(--die-cut);
  text-decoration: none;
}
.spread-meta { font-size: .85rem; color: color-mix(in srgb, var(--board) 70%, var(--stock)); margin: .6rem 0 0; }

/* THE HERO PLATE. The frame lives HERE and not inside the SVG, deliberately:
   a "2px offset shadow" drawn inside a viewBox scales with the column, so it
   would be 2px on a 320px phone and 5px on a laptop -- and a printed offset
   that changes weight with the window is not a printed offset. Border, shadow
   and die-cut are the same three declarations every card on this site uses,
   so the plate reads as a piece of card on the board, not a picture in a box.

   The generated drawing is 320x120, so `height: auto` fixes it at 8:3 and it
   can never push the title off a phone. Authored art keeps its own ratio: it
   is a drawing of a real thing, and cropping it to a band would be a layout
   decision overruling an illustrator. */
.hero-plate {
  margin: 0 0 1.2rem;
  background: var(--stock-deep);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  overflow: hidden;
  line-height: 0;          /* kills the inline-box gap under the <svg> */
}
.hero-plate svg { display: block; width: 100%; height: auto; color: var(--ink); }
/* Authored art was drawn to sit on the page ground, not on a deeper card. */
.hero-authored { background: var(--stock); padding: .5rem; }

/* Numbered turns — the rulebook's own device. */
body[data-kind="article"] main h2 { counter-increment: turn; position: relative; padding-left: 2.6rem; }
body[data-kind="article"] main { counter-reset: turn; }
body[data-kind="article"] main h2::before {
  content: counter(turn);
  position: absolute; left: 0; top: -.05em;
  width: 1.9rem; height: 1.9rem;
  display: grid; place-items: center;
  font-size: .95rem; font-family: var(--font-display);
  background: var(--ink); color: var(--stock);
  border: 2px solid var(--rule); border-radius: 50%;
}

/* ------------------------------------------------------------- callouts -- */

.callout, .steps, .materials {
  border: var(--stroke) solid var(--rule); border-radius: var(--die-cut);
  padding: 1rem 1.15rem; margin: 1.6rem 0; background: var(--ink-soft);
  box-shadow: var(--shadow);
}
.callout::before, .steps::before, .materials::before {
  display: block; font-family: var(--font-display); text-transform: uppercase;
  font-size: .75rem; letter-spacing: .07em; margin-bottom: .5rem; color: var(--ink-edge);
}
.callout-fact::before   { content: "Did you know"; }
.callout-safety::before { content: "Grown-up needed"; }
/* "What to do", not "How to play". The block was named for the rulebook world
   and labelled for a game, and MEASURED, 31 articles now carry `::: steps` and
   NONE of them is a game -- all four sessions converged on this block for
   instructions, which is what it is for. A bird-feeder build captioned HOW TO
   PLAY was the one visibly wrong thing left on those pages.
   It pairs with "What you need" below, which is the same voice. */
.steps::before          { content: "What to do"; }
.materials::before      { content: "What you need"; }
.callout-safety { background: color-mix(in srgb, var(--ink-arts) 12%, var(--stock)); border-color: var(--ink-arts); }
.callout-safety::before { color: var(--ink-arts); }
.callout > :last-child, .steps > :last-child, .materials > :last-child { margin-bottom: 0; }

figure.diagram { margin: 1.8rem 0; text-align: center; }
figure.diagram svg { max-width: 100%; height: auto; color: var(--ink); }

/* Belt, not the fix. render.py strips fixed pixel sizes off every article SVG
   so none CAN overflow; this only bounds one that arrives some other way.
   Scoped to article bodies -- the board motifs size themselves deliberately. */
body[data-kind="article"] main svg { max-width: 100%; height: auto; }
figcaption { font-size: .9rem; margin-top: .6rem; color: color-mix(in srgb, var(--board) 72%, var(--stock)); }

/* ------------------------------------------------------------- listings -- */

.article-list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--gap); grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); }
.article-list .card {
  background: var(--stock); border: var(--stroke) solid var(--rule); border-radius: var(--die-cut);
  box-shadow: var(--shadow); padding: 1rem; display: flex; flex-direction: column; gap: .4rem;
  position: relative;
}
/* THE WHOLE CARD IS THE TARGET, not the line of text at the top of it.
   MEASURED: the title link was 28px tall, against a 44px fingertip, and it
   was the only interactive part of the card -- so a child tapping the obvious
   big rectangle got nothing, which at 7 reads as "broken" rather than "missed".
   A stretched pseudo-element keeps ONE link and one accessible name (a wrapper
   round the whole card would put the summary text inside the link name), and
   leaves the summary selectable. */
.article-list .card > a::after { content: ""; position: absolute; inset: 0; }
/* The card, not the text, shows the focus ring, since the card is the target. */
.article-list .card:focus-within { outline: var(--stroke) solid var(--ink); outline-offset: 3px; }
.article-list .card > a:focus-visible { outline: none; }
/* MEASURED: search reported "1 thing matches" while all 76 stayed on screen.
   `display: flex` above outranks the user-agent's [hidden] { display: none },
   so setting .hidden in JS did nothing visible. Any author display rule on an
   element that is ever hidden has to say so itself. */
.article-list .card[hidden] { display: none; }
.article-list .card a { font-family: var(--font-display); font-size: 1.05rem; color: var(--board); text-decoration: none; }
.article-list .card a:hover { color: var(--ink-edge); text-decoration: underline; }
.article-list .summary { font-size: .9rem; margin: 0; color: color-mix(in srgb, var(--board) 76%, var(--stock)); }

.breadcrumb { max-width: var(--measure); margin: 1.2rem auto .4rem; padding: 0 var(--gap); font-size: .85rem; }

/* --------------------------------------------------------------- footer -- */

.site-footer {
  border-top: var(--stroke) solid var(--rule); margin-top: 4rem;
  background: var(--stock-deep);
  padding: 1.6rem max(var(--gap), env(safe-area-inset-right))
           max(1.6rem, env(safe-area-inset-bottom)) max(var(--gap), env(safe-area-inset-left));
  font-size: .85rem;
}
.site-footer .inner { max-width: 62rem; margin-inline: auto; display: flex; flex-wrap: wrap; gap: .6rem 1.5rem; }
.site-footer a { color: var(--board); }

/* ------------------------------------------------------------ reduced motion -- */

@media (max-width: 34rem) {
  /* Safe areas have to be repeated here. This rule used to override the base
     masthead padding wholesale, which switched the insets OFF below 34rem --
     phone widths, the only place a notch exists. MEASURED: computed padding
     came back .7rem/.9rem rather than the safe-area max(), so the fix was
     inert exactly where it was needed. */
  .masthead {
    padding: max(.7rem, env(safe-area-inset-top)) max(.9rem, env(safe-area-inset-right))
             .7rem max(.9rem, env(safe-area-inset-left));
  }
  /* THE BANNER WAS EATING THE PAGE. MEASURED on the home page: 277px tall on
     a 390px phone (33% of the fold) and 43% on a 360x640 Android, because
     seven pills wrapped to three rows above everything. The board -- which
     DESIGN.md calls the first viewport -- started 73% of the way down a
     360x640 screen, so a child saw NO board at all.

     One row that scrolls sideways, not three that stack. Every subject stays
     reachable and visible-by-swiping; nothing is hidden behind a hamburger,
     which for this audience is the worst option -- a 7-year-old does not know
     that three lines mean "the rest of the site is in here". */
  .masthead nav {
    width: 100%; margin-left: 0;
    flex-wrap: nowrap; overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
  }
  .masthead nav a {
    font-size: .72rem; padding: .35rem .6rem;
    flex: 0 0 auto; scroll-snap-align: start;
  }
  /* Swap to the short labels here, at the same width the nav reflows. */
  .masthead nav .nav-long  { display: none; }
  .masthead nav .nav-short { display: inline; }
  .article-list { grid-template-columns: 1fr; }
}

/* ONE ROW EVERYWHERE. THE AFFORDANCE IS WHAT DIFFERS.

   The row above used to be `nowrap; overflow-x: auto` with the scrollbar
   HIDDEN at every narrow width, on the reasoning that a half-visible pill is
   its own affordance. That is true of a finger and false of a mouse: on a
   narrow desktop window there is nothing to swipe and no bar to drag, so four
   of the eight subjects were unreachable by any means. Reported from a real
   non-touch machine.

   The first fix wrapped the row for fine pointers. Measured, that cost three
   rows and a 223px banner at 320px -- reachable, but it rebuilt the wall the
   single row was introduced to remove. So the split is not layout, it is the
   CONTROL: one row for everyone, and a real scrollbar for the pointer that
   needs one.

   Sized and inked deliberately: 10px with a 2px rule, not a hairline, because
   the person dragging it may be seven. Keyboard users need neither -- Tab
   scrolls a pill into view on its own. */
@media (max-width: 34rem) and (pointer: coarse) {
  .masthead nav { scrollbar-width: none; }
  .masthead nav::-webkit-scrollbar { display: none; }
}

@media (max-width: 34rem) and (not (pointer: coarse)) {
  .masthead nav { padding-bottom: .3rem; scrollbar-width: thin;
                  scrollbar-color: var(--ink) var(--stock-deep); }
  .masthead nav::-webkit-scrollbar { height: 10px; }
  .masthead nav::-webkit-scrollbar-track {
    background: var(--stock-deep); border: 2px solid var(--rule);
    border-radius: var(--die-cut);
  }
  .masthead nav::-webkit-scrollbar-thumb {
    background: var(--ink); border: 2px solid var(--rule);
    border-radius: var(--die-cut);
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  .region a:hover, .region a:focus-visible { transform: none; }
}

/* ---------------------------------------------------------------- found -- */

/* Marking a thing found. Every element below is CREATED BY found.js and never
   shipped in the HTML, so with JavaScript off none of this exists and nothing
   appears that would not work.

   The state is a SHAPE before it is a colour: an empty token disc becomes a
   filled disc with a tick, and the word next to it says the same thing. A
   child who cannot tell the inks apart loses nothing. */

/* The strip that closes a spread: a printed rule, then your go. */
.found-strip {
  margin: 2.6rem 0 0;
  padding-top: 1.2rem;
  border-top: var(--stroke) solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .7rem 1rem;
}

/* Deliberately the same object as a widget's .bk-btn — same face, same rule,
   same hard offset, same landing press. A second button language on the same
   page would read as a second site. */
.found-btn,
.found-hide {
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1.1;
  color: var(--board);
  background: var(--stock);
  border: var(--stroke) solid var(--rule);
  border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  /* 48px, not 44: DESIGN.md's floor for anything inside a widget, and this is
     the one control on the page a child is meant to hit without aiming. */
  min-height: 48px;
  padding: .55rem 1rem;
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  cursor: pointer;
  text-align: left;
  transition: transform .08s steps(2, end), box-shadow .08s steps(2, end);
}
.found-btn:hover,
.found-hide:hover { background: var(--ink-soft); }

/* The token lands: the card sits down onto the board and its shadow closes. */
.found-btn:active,
.found-hide:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 var(--rule);
}

.found-btn[aria-pressed="true"],
.found-hide[aria-pressed="true"] {
  background: var(--ink);
  color: var(--stock);
}

/* An ink outline vanishes on an ink fill, so the pressed state gets the
   inverse ring plus a second band — the same fix widgets.css makes. */
.found-btn[aria-pressed="true"]:focus-visible,
.found-hide[aria-pressed="true"]:focus-visible {
  outline-color: var(--stock);
  box-shadow: var(--shadow), 0 0 0 6px var(--ink);
}

/* The disc reserves its box in every state, so pressing the button never
   changes its width and nothing on the page shifts under a fingertip. */
.found-disc { width: 1.5em; height: 1.5em; flex: none; }
.found-btn .found-disc { color: var(--ink); }
.found-btn[aria-pressed="false"] .tick { display: none; }
.found-btn[aria-pressed="true"] .ring { fill: var(--stock); stroke: var(--stock); }
.found-btn[aria-pressed="true"] .tick { stroke: var(--ink); }

/* Announced and visible, like .search-status: silence after a press reads as
   "broken" to a child, and colour on its own says nothing to a screen reader.

   NOT `:empty { display: none }`, which is the obvious thing to write and is
   wrong: a live region that is display:none is absent from the accessibility
   tree, and a region that was absent at load is not reliably announced when it
   is later filled -- so the very first press, the one that most needs saying,
   is the one most likely to be silent. It stays in the tree, on its own row,
   and is simply zero-height until it has something to say. Owning the row also
   stops the strip reflowing sideways under a fingertip when text arrives. */
.found-say {
  margin: 0;
  flex: 1 0 100%;
  font-weight: 700;
  font-size: .95rem;
}

/* Not an error, a note. The site still works; one button does not. */
.found-say-off {
  font-weight: 400;
  color: color-mix(in srgb, var(--board) 76%, var(--stock));
  max-width: var(--measure);
}

/* The stamp on a card in a listing. LAST child on purpose: above the title it
   pushed the titles out of line with each other across the grid. */
.found-chip {
  margin: .2rem 0 0;
  display: flex;
  align-items: center;
  gap: .35rem;
  font-family: var(--font-display);
  font-size: .78rem;
  letter-spacing: .02em;
  color: var(--ink);
}
.found-chip .found-disc { width: 1.15em; height: 1.15em; }
.found-chip .ring { fill: none; }

/* The hide control, above the list it acts on. */
.found-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem 1rem;
  margin: 0 0 1.4rem;
}


/* ---------------------------------------------------------------------------
   SHELVES — the tier between a region and an article.

   A region page used to be one undifferentiated grid. Measured, that is fine
   at 13 cards and unusable at 83: a child scans headings, not cards. These
   rules exist to make the heading the thing you scan, which means the count
   must NOT compete with the shelf's name for the same attention.
--------------------------------------------------------------------------- */

.shelf-jump {
  display: flex; flex-wrap: wrap; gap: .5rem;
  margin: 0 0 1.6rem; padding: 0;
}
.shelf-jump a {
  font-family: var(--font-display); font-size: .8rem;
  color: var(--ink); background: var(--stock);
  border: 2px solid var(--rule); border-radius: var(--die-cut);
  box-shadow: var(--shadow);
  padding: .38rem .62rem; text-decoration: none;
}
.shelf-jump a:hover,
.shelf-jump a:focus-visible { background: var(--ink-soft); }

.shelf { margin: 0 0 2.4rem; }
.shelf > h2 {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: .55rem;
  font-size: clamp(1.15rem, 3.4vw, 1.55rem);
  color: var(--ink);
  margin: 0 0 .85rem; padding-bottom: .38rem;
  border-bottom: var(--stroke) solid var(--ink);
}
/* Deliberately NOT the display face: the count is a fact about the shelf, not
   part of its name, and setting it in the same face made the h2 read as
   "Space and the sky 5 things" -- one long title. */
.shelf-count {
  font-family: var(--font-read); font-size: .76rem; font-weight: 700;
  color: var(--board); background: var(--stock-deep);
  border: 2px solid var(--rule); border-radius: var(--die-cut);
  padding: .1rem .42rem;
}

/* The activity chip. Unstyled it rendered as a bare word under the title --
   "Play" read as an instruction rather than a label, which is worse than no
   chip at all. Solid ink block, stock text: the inks were already darkened to
   clear AA against stock, so this direction inherits that. */
.activity-chip {
  /* `align-self` is doing the work, not `display`. The card is a flex column,
     so a block child stretches to the full card width whatever its display
     value -- the chip rendered as a full-width bar reading "Play", which looks
     like a button and is not one. Same stretch that made the board tiles
     ragged; the fix belongs on the child either way. */
  display: inline-block; align-self: flex-start; margin: .4rem 0 0;
  font-family: var(--font-display); font-size: .66rem; letter-spacing: .05em;
  color: var(--stock); background: var(--ink);
  border: 2px solid var(--rule); border-radius: var(--die-cut);
  padding: .12rem .45rem;
}
.card .activity-chip { position: relative; }

@media (max-width: 30rem) {
  .shelf-jump { gap: .4rem; margin-bottom: 1.2rem; }
  .shelf-jump a { font-size: .74rem; padding: .32rem .5rem; }
  .shelf { margin-bottom: 1.9rem; }
}


/* MEASURED: the Caesar-cipher article overflowed a 320px screen by 38px, and
   the game on it was not the cause -- removing the whole game mount left the
   overflow at exactly 38. Two unbreakable 26-letter alphabet strings in the
   prose were wider than the column, and a child cannot scroll to what is off
   the side of the page.

   Fixed here rather than in that article because the hazard is general: any
   article may quote a long word, a URL or a sequence, and one CSS rule covers
   every one of them where an edit covers only the article somebody noticed.
   `anywhere` rather than `break-word` because break-word will not break a
   single token that is on a line by itself, which is exactly this case. */
body[data-kind="article"] main :is(p, li, dd, figcaption) {
  overflow-wrap: anywhere;
}

/* BUT NOT INSIDE A GAME, and the reason is not the word breaking.
   `anywhere` also changes MIN-CONTENT SIZING, so it can shrink a flex or grid
   track rather than only rewrapping a sentence -- and a game's status and note
   are <p> inside <main>, so the rule above reaches them on an article page and
   NOT on /games/. A game board could therefore have been quietly narrower at
   its article mount than on its own page, and passed every test either session
   runs, because the game page is where a game is tested.

   Caught by the session that owns the games, which re-drove all six at their
   ARTICLE mounts after reading this rule; all six were fine. This makes that a
   property rather than a fact about the six that exist today -- a game builds
   its own layout deliberately, and prose wrapping is not ours to impose on it.

   Specificity is deliberate: the reset needs the same `body ... main` prefix
   to outweigh the rule above, which a bare `.game-mount` selector would not. */
body[data-kind="article"] main .game-mount :is(p, li, dd, figcaption) {
  overflow-wrap: normal;
}
