/* artGlobal shell — miakandijames.com
 *
 * Colour is defined once, here, as tokens by role. Nothing below the token
 * blocks names a literal colour.
 *
 * THE THESIS
 *   The ground is hand-cut and loud. Everything built on it is exact.
 *   Two photographs of real tile carry the page; every value laid over them
 *   comes from a scale. That contrast is the design.
 *
 * THE RULE THAT SHAPES THE LAYOUT
 *   star-layout.jpg is internally 8.19:1 (cobalt field, near-white stars).
 *   No scrim makes text safe on it — grey at 0.90 alpha still only reaches
 *   3.96:1. So NO TEXT EVER SITS ON THE STAR FIELD. Every word rides on an
 *   opaque plane at 5.64:1. The pattern frames; it never fills.
 *
 * See docs/plans/03-artglobal-shell-plan.md for the measured contrast table.
 */

:root {
  /* ---- the seven, from brand/wander-output.md §2 ---- */
  --lavender: #CCA9EB;
  --sage:     #9FCCBA;
  --nile:     #C8E7E9;
  --paper:    #F7F5F0;
  --grey:     #5A6270;
  --cerulian: #008EA5;
  --deep:     #1843A8;

  /* ---- tool tiles. A tool that brings its own mark may bring its own ground.
         These are NOT additions to the seven: they belong to the tool, and they
         exist because a mark drawn for another palette can disappear on --deep.
         Nebula's spectrum is the case in point — its cobalt stop #264BA4 is
         1.09:1 on --deep, and its sky and aqua stops are 1.53 and 2.06. On the
         cream field the palette was drawn for, every stop clears 3:1.
         A tool with no mark of its own keeps --deep and takes no token here. ---- */
  --tile-cream:     #FFF9F3;   /* Nebula's field */
  --tile-cream-rim: #A3866E;   /* Nebula's rule-firm. Cream on --surface is 1.04:1 — no
                                  edge at all — so the tile draws its own: 3.11:1 on
                                  --surface, 3.25:1 on the cream. Clears the 3:1 UI bar
                                  from both sides. */

  /* ---- derived. Hue-matched to --grey (both ~218°), because black was
         rejected in wander-output.md §7. Not new colours, lower values. ---- */
  --slate:      #2E333C;
  --slate-lift: #363B44;
  --paper-dim:  #E4E2DC;

  /* ---- roles ---- */
  --field:      var(--grey);
  --surface:    var(--paper);
  /* The band the tools sit ON.
   *
   * LIGHT KEEPS THE WHITE. The shelf is the same --surface as the tools, and the
   * separation is carried by --shadow-rest alone. That works here because a
   * shadow is dark and the ground is near-white: it has the full value range of
   * the page to fall through, and it reads at a glance.
   *
   * A darker band was tried and rejected. It bought 1.19:1 against the card —
   * a whisper — so it was never what made a tool read as an object, and it cost
   * the collection the cream that the hero and the About plane are also cut
   * from. Elevation was doing the work; the fill step was taking the credit.
   *
   * DARK IS DIFFERENT, AND DELIBERATELY SO. A shadow in dark mode is dark on
   * dark: it has almost no range left to fall through, so it cannot carry the
   * separation by itself the way it does in light. There the shelf drops to
   * --slate and the tools stay --slate-lift, and the value step does the job the
   * shadow cannot. Not an inversion of light — a different answer to the same
   * question, because the conditions are not the same. */
  --band:       var(--surface);
  --ink:        var(--grey);   /* on --surface 5.64:1 */
  --ink-strong: var(--deep);   /* on --surface 8.01:1 */
  --focus:      var(--deep);   /* every interactive element sits on a light plane */
  --rule:       rgba(90, 98, 112, .22);   /* --grey, thinned */
  --rule-firm:  rgba(90, 98, 112, .42);

  /* ---- scrims. The page-ground alpha is tuned by eye: its only floor is
         that the tile must still read as tile. No text sits on it. ---- */
  --field-scrim: rgba(90, 98, 112, .30);   /* the haze. --grey, thinned, so the cobalt
                                              tile sits back into the field instead of
                                              shouting off it. Alpha is by eye, not by
                                              contrast: no text is read on the field. */
  --nav-scrim:   transparent;              /* no haze here — the bar carries type */
  --nav-ink:     #0D0F13;                  /* Measured on rendered pixels with the type
                                              hidden: 4.85:1 on the band at --nav-band.
                                              That clears the 4.5 body bar outright, so the
                                              bar does not lean on the large-text exemption.
                                              At dead centre the same ink measures 2.93:1 —
                                              see --nav-band for why. */

  /* ---- pattern scale ---- */
  --field-tile: 168px;
  --nav-h-tile: 0;   /* unused: the band is sized to the bar, auto 100% */

  --shadow:      0 1px 2px rgba(46, 51, 60, .07), 0 10px 30px -14px rgba(46, 51, 60, .30);
  /* A tool at REST. Shorter and tighter than --shadow-lift: the contact shadow
   * that says the tile is a physical thing lying on the shelf, before anyone has
   * touched it. Without this the card announced itself only on hover, which is
   * backwards — hover was a reward for an invitation never sent. */
  /* This is the ONLY thing separating a tool from the shelf in light, since the
   * two share --surface. Three stops rather than one: a hairline contact shadow
   * so the edge sits down on the paper, a short one for the body, and a wide
   * soft one for the lift. A single blur at this strength reads as a smudge. */
  --shadow-rest: 0 1px 1px rgba(20, 26, 38, .09),
                 0 3px 6px rgba(20, 26, 38, .11),
                 0 10px 20px -6px rgba(20, 26, 38, .18);
  /* The same, cast from an alpha channel — for the drawings, whose corners are
   * transparent and would print shadow into empty pixels with box-shadow. */
  --tile-rest:   drop-shadow(0 1px 1px rgba(20, 26, 38, .10))
                 drop-shadow(0 3px 5px rgba(20, 26, 38, .16))
                 drop-shadow(0 9px 15px rgba(20, 26, 38, .20));
  --shadow-lift: 0 2px 4px rgba(20, 26, 38, .14),
                 0 20px 32px -8px rgba(20, 26, 38, .34),
                 0 48px 76px -20px rgba(20, 26, 38, .52);

  /* ---- one spacing scale. Every gap on the page is one of these. ---- */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px; --s5: 24px;
  --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;

  /* ---- one type scale. Six sizes, page-wide. ---- */
  --t1: 11px;
  --t2: 13px;
  --t3: 15px;
  --t4: 19px;
  --t5: 26px;
  --t-mark: 23px;   /* the wordmark. Bar links use --t4 (19px); both are bold and
                     * >= 18.66px, which is WCAG large text, where the bar is 3:1. */
  --t6: clamp(38px, 7vw, 68px);

  --radius: 3px;
  --radius-tool: 14px;
  --tile-lift: drop-shadow(0 2px 3px rgba(20, 26, 38, .16))
               drop-shadow(0 13px 16px rgba(20, 26, 38, .28))
               drop-shadow(0 28px 36px rgba(20, 26, 38, .38));
  /* LIGHT. --cerulian is hue 188 — cyan, not blue — and --paper is a warm cream.
   * A dark cyan on warm cream reads as verdigris, not as a signal, so the light
   * toast rotates 14 degrees to 202 and lifts 8 points of lightness. Saturation
   * stays 1.00, the rule --cerulian was chosen for. It stays 20 degrees clear of
   * --deep (222) so the toast is never mistaken for a link. Carries --paper at
   * 4.60:1 — no large-text exemption — and stands off the cream collection band
   * at the same 4.60:1, which is the job: it has to be seen.
   * The DARK value stays cerulian; see the dark blocks. */
  --toast-bg:  #0B5FFF;   /* electric. Saturation 0.96, and --paper on it is 4.71:1,
                           * so it clears the body bar and not merely large text. */
  --toast-ink: var(--paper);   /* the tool boxes only — they are the app-like objects */
  --border: 1px;
  --wrap: 1120px;          /* reading width: hero, about, footer */
  --shelf: 1350px;         /* the tools, and the bar that aligns to them. Sized so
                            * five columns fit and six cannot: 5x215+4x24 = 1171,
                            * 6x215+5x24 = 1410, content here is 1286. Card width
                            * is (1286-96)/5 = 238px. */
  --shelf-gutter: var(--s6);
  --tool-min: 215px;   /* the floor a box may shrink to before the grid drops a
                        * column. Rendered width comes from --shelf, not this. */   /* smallest a tool box gets before the grid drops a column */
  /* Line measures in px, not ch: a ch is measured in the CURRENT font, so a
   * webfont swap rewraps the text and moves everything below it. */
  --measure-lede: 420px;
  --measure-body: 560px;
  --measure-plane: 720px;
  --nav-h: 150px;   /* the bar IS one whole photograph: background-size auto 100%
                     * snaps the picture to this, so scale and bar height are
                     * the same number and cannot drift apart. */
  --nav-band: 36%;   /* Above the middle line, where Mia wants it. Light and dark
                      * share this one position, so it must serve both glazes.
                      * Measured on RENDERED bars, type hidden:
                      *   36%  light 4.75   dark 3.36 (with #FFF) / 3.09 (--paper)
                      *   64%  light 4.90   dark 4.34 — better, but below centre
                      * The dark glaze is why --nav-ink-dark is pure white rather
                      * than --paper: at this position --paper does not clear 3:1.
                      * RE-MEASURE IF --nav-h, --nav-ink OR EITHER CROP CHANGES. */

  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;

}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --field:       var(--slate);
    --surface:     var(--slate-lift);
    --band:        var(--slate);
    --ink:         var(--paper-dim);
    --ink-strong:  var(--nile);
    --focus:       var(--nile);
    --rule:        rgba(247, 245, 240, .16);
    --rule-firm:   rgba(247, 245, 240, .32);
    --field-scrim: rgba(46, 51, 60, .52);   /* lighter haze than before: the tile reads */
    --nav-scrim:   transparent;
    --nav-ink:     #FFFFFF;           /* the one place a non-palette value is used:
                                       * on the dark glaze at --nav-band, --paper
                                       * measures 3.09:1 and white measures 3.36:1 */
    /* A deepened --cerulian. On the cool, near-neutral --slate-lift the teal is
     * the only chroma in the frame and reads as lit rather than aged, so dark
     * keeps it where light could not. --paper on it is 4.73:1. */
    --toast-bg:    #00788D;
    --shadow:      0 1px 2px rgba(0, 0, 0, .45), 0 10px 30px -14px rgba(0, 0, 0, .70);
    --shadow-rest: 0 1px 1px rgba(0, 0, 0, .34),
                   0 2px 4px rgba(0, 0, 0, .40),
                   0 8px 16px -6px rgba(0, 0, 0, .52);
    --tile-rest:   drop-shadow(0 1px 1px rgba(0, 0, 0, .40))
                   drop-shadow(0 3px 5px rgba(0, 0, 0, .50))
                   drop-shadow(0 9px 15px rgba(0, 0, 0, .58));
    --tile-lift: drop-shadow(0 2px 3px rgba(0, 0, 0, .55))
                 drop-shadow(0 13px 16px rgba(0, 0, 0, .70))
                 drop-shadow(0 28px 36px rgba(0, 0, 0, .80));
    --shadow-lift: 0 2px 4px rgba(0, 0, 0, .60),
                   0 20px 32px -8px rgba(0, 0, 0, .78),
                   0 48px 76px -20px rgba(0, 0, 0, .92);
  }
}

:root[data-theme="dark"] {
  --field:       var(--slate);
  --surface:     var(--slate-lift);
  --band:        var(--slate);
  --ink:         var(--paper-dim);
  --ink-strong:  var(--nile);
  --focus:       var(--nile);
  --rule:        rgba(247, 245, 240, .16);
  --rule-firm:   rgba(247, 245, 240, .32);
  --field-scrim: rgba(46, 51, 60, .52);   /* lighter haze than before: the tile reads */
  --nav-scrim:   transparent;
  --nav-ink:     #FFFFFF;
  /* A deepened --cerulian. On the cool, near-neutral --slate-lift the teal is the
   * only chroma in the frame and reads as lit rather than aged, so dark keeps it
   * where light could not. --paper on it is 4.73:1. */
  --toast-bg:    #00788D;
  --shadow:      0 1px 2px rgba(0, 0, 0, .45), 0 10px 30px -14px rgba(0, 0, 0, .70);
  --shadow-rest: 0 1px 1px rgba(0, 0, 0, .34),
                 0 2px 4px rgba(0, 0, 0, .40),
                 0 8px 16px -6px rgba(0, 0, 0, .52);
  --tile-rest:   drop-shadow(0 1px 1px rgba(0, 0, 0, .40))
                 drop-shadow(0 3px 5px rgba(0, 0, 0, .50))
                 drop-shadow(0 9px 15px rgba(0, 0, 0, .58));
  --tile-lift: drop-shadow(0 2px 3px rgba(0, 0, 0, .55))
               drop-shadow(0 13px 16px rgba(0, 0, 0, .70))
               drop-shadow(0 28px 36px rgba(0, 0, 0, .80));
  --shadow-lift: 0 2px 4px rgba(0, 0, 0, .60),
                 0 20px 32px -8px rgba(0, 0, 0, .78),
                 0 48px 76px -20px rgba(0, 0, 0, .92);
}

/* ================================================================== */

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
html, body { margin: 0; }

/* The field is its own layer, pinned to the viewport behind everything. Every
 * section then floats over it without needing a background of its own. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-color: var(--field);
  background-image: linear-gradient(var(--field-scrim), var(--field-scrim)),
                    url("../img/star-field.jpg");
  background-size: auto, var(--field-tile) auto;
  background-repeat: repeat, repeat;
}

body {
  background: var(--field);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t3);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* one alignment spine: .wrap is the only horizontal authority on the page */
.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding-inline: var(--s5);
}

.skip {
  position: absolute; left: var(--s5); top: calc(var(--nav-h) * -2);
  background: var(--surface); color: var(--ink-strong);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius);
  font-size: var(--t2); font-weight: 600;
  z-index: 20; transition: top .18s ease;
}
.skip:focus { top: var(--s3); }

:where(a, button):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--radius);
}

/* ---- nav band: her polkadot photograph, repeating ---- */

.nav {
  position: sticky; top: 0; z-index: 10;
  height: var(--nav-h);
  background-color: var(--surface);
  background-image: linear-gradient(var(--nav-scrim), var(--nav-scrim)),
                    url("../img/polka-band.jpg");
  background-size: auto, auto 100%;   /* one whole photograph tall, uncropped */
  background-repeat: repeat, repeat-x;
  background-position: center, center;
  border-bottom: var(--border) solid var(--rule-firm);
}


.nav-in {
  position: absolute;
  inset-inline: 0;
  top: var(--nav-band);
  transform: translateY(-50%);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s5);
}

.mark {
  font-size: var(--t-mark);
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--nav-ink);
  text-decoration: none;
  white-space: nowrap;
}

.nav-links { display: flex; align-items: center; gap: var(--s2); }

.nav-links a, .toggle {
  font-family: var(--sans);
  font-size: var(--t4);
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--nav-ink);
  text-decoration: none;
  padding: 0;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  cursor: pointer;
  line-height: 1.1;
}

.nav-links { display: flex; align-items: center; gap: var(--s6); }
.nav-in { align-items: center; }

@media (hover: hover) {
  .nav-links a:hover, .toggle:hover, .mark:hover { border-bottom-color: currentColor; }
}

/* ---- touch targets ----
 *
 * Measured on a rendered iPhone SE: the bar links came out 49x23 and 54x23, the
 * theme toggle 42x23, the footer address 125x23. All four are under the 44px
 * minimum a finger needs, and three of them are under it in BOTH directions.
 *
 * The fix cannot be padding: these are inline links whose underline is a
 * border-bottom, so padding would push the rule 20px below the word and break
 * the bar's look on desktop to serve the phone. Instead each one grows an
 * invisible 44px-tall child centred on the text. The design does not move; the
 * target does. inset-inline is stretched a little past the word as well, so
 * neighbouring links in the bar never leave a dead lane between them.
 *
 * .mark is 146x31 — wide enough, short enough, so it gets the same treatment. */
.nav-links a, .toggle, .mark, .foot a { position: relative; }

.nav-links a::after,
.toggle::after,
.mark::after,
.foot a::after {
  content: "";
  position: absolute;
  inset-inline: calc(var(--s2) * -1);
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* Dark mode swaps the glaze for its dark firing — same tile, same geometry, so
 * --nav-band holds for both. */
:root[data-theme="dark"] .nav { background-image: url("../img/polka-band-dark.jpg"); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .nav { background-image: url("../img/polka-band-dark.jpg"); }
}

/* ---- planes: the only place text is allowed ---- */

.plane {
  background: var(--surface);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* ---- hero ---- */

.hero { padding-block: var(--s8) var(--s6); }
.hero-plane { padding: var(--s7) var(--s7) var(--s8); max-width: 760px; }

.eyebrow {
  font-family: var(--mono);
  font-size: var(--t1);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink);
  margin: 0 0 var(--s4);
}

h1 {
  font-size: var(--t6);
  line-height: .98;
  letter-spacing: -.03em;
  font-weight: 700;
  margin: 0;
  color: var(--ink-strong);
  text-wrap: balance;
}

.lede {
  font-size: var(--t4);
  line-height: 1.45;
  margin: var(--s5) 0 0;
  max-width: var(--measure-lede);
  color: var(--ink);
  text-wrap: balance;
}

.hero-sub { margin: var(--s4) 0 0; max-width: var(--measure-body); }

/* ---- the hero plane IS the door to the portfolio ----
 *
 * The page already has one grammar for "this whole box is pressable": the tool
 * card. A resting shadow is the invitation, a lift is the reward, and a mono
 * label under a hairline says what pressing does. The hero borrows that grammar
 * rather than inventing a second one — if the name behaved differently from the
 * tools, the page would be teaching two interactions on one screen.
 *
 * What changes is the TRAVEL. A 238px card rises 9px and scales 1.02; a 760px
 * plane doing the same reads as the page itself jumping, because the eye judges
 * the lift against the width of the thing lifting. 5px and no scale at this
 * size lands at the same apparent height.
 */
.hero-plane {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  box-shadow: var(--shadow-rest);
  transition: transform .18s cubic-bezier(.2,.8,.3,1), box-shadow .18s ease;
}

/* Gated for the same reason .card:hover is: a tap on iOS leaves :hover stuck on
 * the last thing touched, which would park the hero permanently raised. */
@media (hover: hover) {
  .hero-plane:hover { transform: translateY(-5px); box-shadow: var(--shadow-lift); }
}
.hero-plane:focus-visible { transform: translateY(-5px); box-shadow: var(--shadow-lift); }
/* Pressed into the page, not scaled down like a card: at 760px a .975 scale is
 * a 19px collapse and reads as a glitch. */
.hero-plane:active { transform: translateY(0); box-shadow: var(--shadow-rest); }


/* ---- collection: an opaque band, so the grid has a calm home ---- */

/* The tools sit in their own container, a little wider than the reading width so
 * the grid breathes without drifting far from the hero. The bar shares it, so the
 * wordmark starts on the same line as the artGlobal heading. */
.collection > .wrap,
.nav .wrap { max-width: var(--shelf); padding-inline: var(--shelf-gutter); }

.collection {
  /* The shelf. --band is --surface in light (the white stays) and a step darker
   * than the tools in dark — see --band for why the two themes answer this
   * differently. Text on it: light --ink 5.64:1 / --ink-strong 8.01:1,
   * dark --ink 9.80:1. Legal on both without a plane of its own. */
  background: var(--band);
  border-block: var(--border) solid var(--rule-firm);
  padding-block: var(--s7);
}

.sec-head { margin-bottom: var(--s6); max-width: var(--measure-body); }

h2 {
  font-size: var(--t5);
  line-height: 1.15;
  letter-spacing: -.02em;
  font-weight: 700;
  margin: var(--s3) 0 0;
  color: var(--ink-strong);
}

.sec-head p { margin: var(--s3) 0 0; }

.grid {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  /* auto-fill + a min column means the row count follows the window at every
   * width instead of stopping at whatever the reading container allows. */
  grid-template-columns: repeat(auto-fill, minmax(var(--tool-min), 1fr));
  grid-auto-rows: 1fr;      /* every row the same height, not just every card in a row */
  gap: var(--s5);
}

/* ---- cards. Stars carry all the colour — nothing else here is coloured. ---- */

.card {
  display: flex; flex-direction: column;
  gap: var(--s2);
  padding: var(--s4);
  aspect-ratio: 1;          /* the first tool's proportion, 276x273, made the rule */
  min-height: 0;
  background: var(--surface);
  border: var(--border) solid var(--rule-firm);
  border-radius: var(--radius-tool);
  /* A tool sits ON the shelf. Without this the card and the band were the same
   * token — byte-identical in dark, #363B44 on #363B44 — and a 1px hairline was
   * the only thing saying "object". The fill step under it is 1.19:1 (light) /
   * 1.13:1 (dark), far too small to carry the separation alone; this is what
   * actually does it. */
  box-shadow: var(--shadow-rest);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  /* Every tool that is not live is a real <button>, so the UA styling has
   * to be undone: a button centres its text and brings its own font. */
  font: inherit;
  text-align: left;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  position: relative;   /* so the lifted box casts over its neighbours */
  transition: transform .18s cubic-bezier(.2,.8,.3,1), box-shadow .18s ease;
}


/* Hover is a REWARD, not the invitation. The invitation is --shadow-rest above,
 * which a touch device never sees any other way.
 *
 * Gated behind (hover: hover) because a tap on iOS leaves :hover stuck on the
 * last thing touched until you tap elsewhere — so an ungated lift means a phone
 * ends up with one card permanently raised and casting. :active and
 * :focus-visible carry touch and keyboard instead, and both are ungated. */
@media (hover: hover) {
  .card:hover {
    transform: translateY(-9px) scale(1.02);
    box-shadow: var(--shadow-lift);
    z-index: 1;
  }
  .card--tile:hover {
    transform: translateY(-9px) scale(1.02);
    box-shadow: none;
    filter: var(--tile-lift);
    z-index: 1;
  }
}

.card:focus-visible {
  transform: translateY(-9px) scale(1.02);
  box-shadow: var(--shadow-lift);
  z-index: 1;
}
.card--tile:focus-visible {
  transform: translateY(-9px) scale(1.02);
  box-shadow: none;
  filter: var(--tile-lift);
  z-index: 1;
}

/* Touch has no hover, so the press itself has to answer. Scaling DOWN is the
 * right direction: the finger is pushing the tile into the shelf. */
.card:active        { transform: scale(.975); box-shadow: var(--shadow-rest); }
.card--tile:active  { transform: scale(.975); filter: var(--tile-rest); }

/* a live tool is a link and says so; one that is still on the way lifts too,
 * so the grid feels alive, but keeps the default cursor and no href */

/* The tool's icon. A classic app icon is a rounded square holding one glyph on
 * a deep ground — which is exactly what wander-output.md §1 says a tool is:
 * deep blue ground, and the star carrying all the colour. */
.card .icon {
  flex: none;                       /* a flex item in a column shrinks; the icon must not */
  width: 52px; height: 52px;
  border-radius: 12px;              /* ~23% of the square, the classic proportion */
  /* --deep unless the tool brought its own ground; see the tile tokens above. */
  background: var(--tile, var(--deep));
  display: grid; place-items: center;
  /* The rim is first so it sits over the fill. It is transparent on every tile
     that does not ask for one, so this costs the other six nothing. */
  box-shadow: inset 0 0 0 1px var(--tile-rim, transparent),
              inset 0 1px 0 rgba(247, 245, 240, .22),
              0 2px 5px rgba(20, 26, 38, .28);
}

.card .star { width: 28px; height: 28px; display: block; }
.card .star path { fill: var(--star); }   /* 4.34-6.67:1 on --deep */
/* A tool's own mark is an <img>, so --star never reaches it and must not: the
   mark carries its own colour. Sized by the same rule above. */
.card img.star { object-fit: contain; }

.card h3 {
  font-size: var(--t4);
  line-height: 1.2;
  letter-spacing: -.015em;
  font-weight: 600;
  margin: var(--s2) 0 0;
  color: var(--ink-strong);
}

.card p {
  margin: 0; font-size: var(--t2); line-height: 1.45;
  /* The boxes are a fixed square. Blurbs are written to fit two lines at the
   * smallest card the grid makes (204px at 1180); the clamp is a safety net so
   * an over-long one degrades cleanly instead of bursting the box. flex:none
   * stops the column squeezing it into a half-visible line. */
  flex: none;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}

.card .state,
.hero-plane .state {
  margin-top: auto;
  font-family: var(--mono);
  font-size: var(--t1);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink);
  padding-top: var(--s3);
  border-top: var(--border) solid var(--rule);
}

/* The hero's label runs the full width of the plane so its hairline closes the
 * box, the way it closes a card. Written AFTER the shared block on purpose:
 * both selectors are (0,2,0), so source order is the only thing deciding which
 * margin-top survives. */
.hero-plane .state { align-self: stretch; margin-top: var(--s6); }

/* ---- an out-of-order tool IS the drawing ----
 *
 * The drawing's corners are transparent, so box-shadow is wrong here: it casts
 * from the element's box and would print shadow into the corners where there is
 * no art. filter: drop-shadow() casts from the alpha channel instead, so the
 * shadow follows the drawn edge exactly and leaves no square behind it.
 * drop-shadow takes no spread, so the offsets and blurs are tightened by eye
 * rather than copied from the box-shadow tokens.
 *
 * ONE DRAWING. The second sign — the plain cone — is gone, along with the
 * `sign` field that chose between them. Every out-of-order tool wears the same
 * face.
 *
 * AND NO TAG. The hung paper tag that used to carry the words "OUT OF ORDER"
 * off the top-left corner is gone, with its two pseudo-elements, its swing, and
 * the --tag-* tokens that coloured it. The drawing already says the tool is out
 * of service — it is a drawing of a sign — so the tag was labelling a label.
 * The state still reaches a screen reader through the button's aria-label, so
 * nothing was lost but the paint.
 */

.card--tile {
  background-color: transparent;
  background-image: url("../img/out-of-order-butt.webp");
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  border: 0;
  padding: 0;
  box-shadow: none;              /* the square one off: the art has no square */
  filter: var(--tile-rest);      /* the alpha-shaped one on, at rest */
  transition: transform .18s cubic-bezier(.2,.8,.3,1), filter .18s ease;
}

:root[data-theme="dark"] .card--tile { background-image: url("../img/out-of-order-butt-dark.webp"); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .card--tile { background-image: url("../img/out-of-order-butt-dark.webp"); }
}

/* ---- toast: what a tool says back when it is not ready ---- */

.toast {
  /* Absolute, in document coordinates: shell.js pins it to the button that was
   * pressed, so the reply comes from the thing you touched. */
  position: absolute;
  left: 0;
  top: 0;
  /* the toast lifts itself clear by its own height — CSS knows it exactly,
   * so JS never has to measure it */
  --toast-y: calc(-100% - 12px);
  transform: translate(-50%, var(--toast-y));
  z-index: 50;
  max-width: min(22em, calc(100vw - var(--s6) * 2));
  white-space: normal;
  pointer-events: none;
  padding: var(--s4) var(--s5);
  background: var(--toast-bg);
  color: var(--toast-ink);
  border-radius: var(--radius-tool);
  box-shadow: var(--shadow-lift);
  font-size: var(--t4);
  font-weight: 700;
  letter-spacing: -.01em;
  line-height: 1.25;
  text-wrap: balance;
  opacity: 0;
  visibility: hidden;
  transition: visibility .16s;
}

/* An animation, not a transition, so a second press REPLAYS it — that replay is
 * the whole point: it is how you know the click landed twice. */
@keyframes toast-in {
  0%   { opacity: 0; transform: translate(-50%, var(--toast-y)) scale(.86); }
  55%  { opacity: 1; transform: translate(-50%, var(--toast-y)) scale(1.06); }
  100% { opacity: 1; transform: translate(-50%, var(--toast-y)) scale(1); }
}

.toast.is-on {
  opacity: 1;
  visibility: visible;
  animation: toast-in .36s cubic-bezier(.2, 1.25, .4, 1) both;
}

.toast.is-below { --toast-y: 12px; }

/* the little pointer, so it reads as coming from the button */
.toast::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -5px;
  width: 10px; height: 10px;
  background: var(--toast-bg);
  transform: translateX(-50%) rotate(45deg);
}

.toast.is-below::after { bottom: auto; top: -5px; }

/* ---- about ---- */

.about { padding-block: var(--s9); }
.about .plane { padding: var(--s7); max-width: var(--measure-plane); }
.about h2 { margin-top: 0; }
.about p { margin: var(--s4) 0 0; }

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

.foot {
  background: var(--surface);
  border-top: var(--border) solid var(--rule-firm);
  padding-block: var(--s6);
}

.foot-in {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  align-items: baseline; justify-content: space-between;
}

.foot a { color: var(--ink-strong); }

.foot small {
  font-family: var(--mono);
  font-size: var(--t1);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink);
}

/* ---- drift: REMOVED 2026-10-17, by Mia's decision. The marks are still.
 *
 * The shelf had every icon on a permanent slow orbit (wander move 24). It was
 * reversed on sight: a grid of marks all moving at once reads as shaking, not
 * as floating. Nothing replaces it — stillness is the decision, not a gap
 * waiting for a different animation.
 *
 * The --drift-dur and --drift-delay custom properties are gone from shell.js
 * with it. The reduced-motion block near the end of this file stays: it covers
 * the toast and the hover transitions, which still move. ---- */

/* ---- narrow ---- */

/* ================================================================== */
/* ---- the phone ----
 *
 * Everything below was written against rendered pages at 375x667, 430x932 and
 * 744x1133, not against a guess about what a phone is.
 */

@media (max-width: 720px) {
  :root {
    --nav-h: 108px;
    --field-tile: 132px;
    --t-mark: 20px;
    --tool-min: 240px;
    /* The bar stacks into two rows here, so the type is no longer a single line
     * to be positioned against the photograph — it is a block that has to sit in
     * the middle of one. 36% was chosen for a one-line bar; at two rows it puts
     * the stack through the top edge.
     *
     * MEASURED, not assumed. Method: render the bar at 375px, hide .nav-in, and
     * sample every glaze pixel inside the rectangles the four bits of type
     * actually occupy. Mean glaze under the type, worst of the four:
     *
     *            light (#0D0F13)   dark (#FFFFFF)
     *   desktop 36%     5.50:1          6.34:1
     *   mobile  50%     5.17:1          7.10:1
     *
     * So the phone bar sits within a third of a point of the desktop bar in
     * light and well above it in dark. The type here is --t4 and --t-mark, both
     * bold and >= 18.66px, so the bar owed is 3:1 and both clear 4.5:1 besides.
     *
     * KNOWN, AND NOT NEW: on a worst-SINGLE-PIXEL basis both bars fall to
     * 1.03-1.63:1, because the photograph runs bright grout and dark dots
     * straight under the words. That is a property of setting type over this
     * tile at all, it is identical on desktop, and it is not something this
     * breakpoint introduced. Fixing it means giving the bar a scrim, which
     * --nav-scrim deliberately sets to transparent. Left as it stands.
     * RE-MEASURE IF --nav-h, --nav-ink, THE STACK OR EITHER CROP CHANGES. */
    --nav-band: 50%;
    /* The reading measures are desktop numbers; on a 375px screen they are wider
     * than the screen and do nothing but sit there as dead constraints. */
    --measure-lede: none;
    --measure-body: none;
  }

  /* The gap is a TOUCH measurement, not a taste one. Stacked, the wordmark and
   * the links each carry a 44px target centred on their text; those two zones
   * collide unless the text centres are 44px apart. Wordmark 31px tall, links
   * 23px, so the gap owes 44 - 15.5 - 11.5 = 17px minimum. --s5 is the first
   * step on the scale that clears it, and 31 + 24 + 23 = 78px still sits inside
   * the 108px bar. At --s3 the links stole 7px off the bottom of the wordmark. */
  .nav-in { flex-direction: column; align-items: flex-start; gap: var(--s5); }
  .nav-links { gap: var(--s5); }

  .wrap { padding-inline: var(--s4); }
  .collection > .wrap, .nav .wrap { padding-inline: var(--s4); }

  .hero { padding-block: var(--s6) var(--s5); }
  .hero-plane { padding: var(--s6) var(--s5); }
  .collection { padding-block: var(--s6); }
  .about { padding-block: var(--s7); }
  .about .plane { padding: var(--s6) var(--s5); }

  /* THE SQUARE HAS TO GO.
   *
   * aspect-ratio: 1 was set from the first tool's proportion and it is right on
   * desktop, where a card is 238px wide. In one column on a phone the card is
   * 311px wide, so the square makes it 311px TALL to hold about 90px of type —
   * two thirds of every card is empty, and six of them made the page 6690px
   * long. The card goes back to being as tall as what is in it.
   *
   * The DRAWINGS keep their square: they are pictures, and 700x700 is the
   * picture. That is also what now distinguishes a built tool from an unbuilt
   * one at a glance in a single column. */
  .card { aspect-ratio: auto; }
  .card--tile { aspect-ratio: 1; }

  /* grid-auto-rows: 1fr exists so that cards sitting in the same ROW share a
   * height. In one column there is no row to match — and 1fr then stretches
   * every row to the tallest thing in the grid, which is a 343px square
   * drawing. The text cards are 190px, so each one sat in its row with 150px of
   * dead air under it. Measured: it made the page 3437px long on an iPhone SE.
   * Auto rows are the honest answer once the grid is a list. */
  .grid { gap: var(--s4); grid-auto-rows: auto; }

  /* The blurb is clamped to two lines because the desktop card is a fixed
   * square with room for exactly two. A phone card has no such ceiling, so the
   * clamp only truncates sentences that had somewhere to go. */
  .card p { -webkit-line-clamp: none; display: block; }

  /* The toast is --t4 with generous padding; at 375px that is nearly the whole
   * screen, and it is pinned over the card that was pressed. Bringing it down a
   * step keeps it a reply rather than a takeover. */
  .toast { font-size: var(--t3); padding: var(--s3) var(--s4); max-width: calc(100vw - var(--s5) * 2); }
}

/* ================================================================== */
/* ---- the home screen ----
 *
 * On a phone a tool stops being a card and becomes an APP ICON: the rounded
 * square, the mark, nothing else. No name, no blurb, no state line.
 *
 * THE THRESHOLD IS 560px, AND IT IS A DEVICE LINE, NOT A TASTE ONE. Every
 * iPhone in portrait is 440px wide or narrower (SE 375, 16 Pro Max 440); the
 * narrowest tablet portrait is 744px (iPad mini). 560 sits in the empty space
 * between the two, so a phone always gets icons and a tablet never does. The
 * 720px block above keeps running underneath this one — it owns the bar, the
 * hero and the toast, which are the same on both sides of this line.
 *
 * THE GRID IS SIZED FROM AN ICON, NOT FROM A COLUMN COUNT. iOS pins itself to
 * four columns; auto-fill at a 72px floor lands on four at every phone width
 * (375px gives 343px of content: (343+16)/(72+16) = 4.07) and quietly becomes
 * five or six on a wide foldable instead of blowing the icons up to fill it.
 * The icon itself is capped at 64px — iOS draws 60pt — so the spacing grows and
 * the object never does.
 *
 * 64px is also well past the 44px touch minimum, which is why the card can
 * shrink to the icon rather than keeping a padded box around it.
 *
 * THE NAME IS STILL THERE FOR A SCREEN READER. h3 is clipped, not removed: a
 * link whose only visible content is an aria-hidden SVG would otherwise have no
 * accessible name at all. The blurb and the state line are display:none — the
 * state is already in the out-of-order drawing and in the toast. */

@media (max-width: 560px) {
  .grid {
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: var(--s5) var(--s4);
  }

  /* The card IS the icon now: no plane, no rim, no padding, no elevation of its
   * own — .card .icon brings its own.
   *
   * Centred with auto margins, NOT with justify-items on the grid. The cells
   * have to stay stretched: justify-items:center shrink-wraps the <li> to its
   * content, and an out-of-order tile has no content at all — it is an empty
   * button wearing a background image — so it shrink-wrapped to 0x0 and the two
   * drawings vanished from the grid while the four icons rendered fine. */
  .card {
    width: 100%;
    max-width: 64px;
    margin-inline: auto;
    aspect-ratio: 1;
    padding: 0;
    gap: 0;
    /* background-COLOR, not the shorthand: `background: none` also clears
     * background-image, and this rule sits after .card--tile in the file, so the
     * shorthand silently wiped the out-of-order drawing off its own tile. */
    background-color: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .card .icon {
    width: 100%; height: 100%;
    border-radius: 23%;    /* the same proportion the 52px tile uses, held at any size */
  }
  .card .star { width: 54%; height: 54%; }

  /* Clipped, not hidden: the accessible name. */
  .card h3 {
    position: absolute;
    width: 1px; height: 1px;
    margin: 0; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .card p, .card .state { display: none; }

  /* An out-of-order tool takes the rounded square too, and the drawing becomes
   * the art INSIDE it.
   *
   * On desktop the drawing floats free, no box, because its silhouette is the
   * thing that says "not a tool". At 64px there is no silhouette left to read —
   * rendered, it was a grey smudge in light and, in dark, a blue square barely
   * distinguishable from the icons beside it. A home screen is a grid of
   * rounded squares anyway, so the honest phone answer is to let it be one: a
   * charcoal photograph against five flat cobalt tiles is a bigger difference
   * at this size than any outline.
   *
   * cover, not contain: the drawings are square (700x700), so this crops
   * nothing — it just fills the tile to the rounded edge instead of leaving the
   * transparent margin that made the thing look like it was floating.
   *
   * The alpha-shaped drop-shadow goes with the free silhouette; the tile takes
   * the same elevation .card .icon carries, so the two sit at one height. */
  .card--tile {
    max-width: 64px;
    background-size: cover;
    border-radius: 23%;
    filter: none;
    box-shadow: inset 0 1px 0 rgba(247, 245, 240, .22),
                0 2px 5px rgba(20, 26, 38, .28);
  }
  .card--tile:active { filter: none; }
}

/* Short and wide — a phone on its side. The bar is a photograph 108px tall, and
 * two of those plus the hero is the entire viewport. Nothing else needs to
 * change; the bar just has to stop eating the screen. */
@media (max-width: 900px) and (max-height: 500px) {
  :root { --nav-h: 84px; }
  .nav { position: static; }   /* sticky at this height leaves nothing to read */
}

/* Constant motion triggers vestibular symptoms. Not a preference —
 * a requirement. brand/wander-output.md §5. Last rule in the file. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation: none !important; transition: none !important; }
}
