/* The intro screen. Owns nothing else: every selector here is scoped under
 * .intro or .intro-scroll, so this file can be added and removed without
 * touching a pixel of the app.
 *
 * IT SITS ON THE APP'S OWN BACKGROUND and adds its own light rather than its
 * own surface. styles.css paints the shell (a dark base with soft colour washes
 * over it), so this file assumes a very dark canvas, keeps its sections
 * transparent, and puts the only glow where the eye is meant to go -- behind
 * the board. Cards are var(--card) so the light theme comes out right for free.
 *
 * MOTION: transform and opacity only, on every animated rule in this file.
 * Nothing here animates width, height, top or margin, because a reveal that
 * costs layout is a reveal that stutters on the phone this app is for. The
 * reveal itself is a class toggled by IntersectionObserver in intro.js; this
 * sheet only says what arriving looks like.
 *
 * REDUCED MOTION: .intro.still is set by intro.js when the learner asks for it,
 * and the block at the bottom of this file makes the whole screen static --
 * everything already in its final state, no transitions, no cue, no glow pulse.
 * styles.css also blanket-kills animation under the media query; the class is
 * belt as well as braces, and it is what makes the revealed state the DEFAULT
 * rather than leaving elements stuck at opacity 0 if a transition never runs.
 */

/* The app's scroll pane, unpadded: the sections here pad themselves so the
   glow and the picks can run wider than a 1rem gutter. overflow-x is pinned
   because `overflow-y: auto` alone leaves the other axis scrollable, and one
   overhanging decoration would give a phone a horizontal scrollbar. */
.intro-scroll {
  padding: 0;
  overflow-x: hidden;
  overscroll-behavior: contain;
}

/* ---------- WHICH SCREEN THIS LOAD IS ----------
 *
 * The landing ships inside #app as real HTML, and #app is also where the app
 * renders. On any route but the landing, app.js overwrites it -- but the
 * browser can paint before that happens, and an installed app opening at
 * /#/learn must never show a frame of the page selling it the thing it already
 * installed.
 *
 * So the landing is hidden by an attribute that index.html's boot script sets
 * from location.hash BEFORE the first paint, and app.js keeps in step on every
 * navigation afterwards (that second half is what makes the back button work:
 * putting the markup back is not enough if this rule is still hiding it).
 *
 * WITH SCRIPTING OFF THE ATTRIBUTE IS NEVER SET AND THE LANDING SHOWS, which is
 * the whole point -- that is the state a crawler is in. */
:root[data-rfx-route="app"] .intro-scroll { display: none; }

.intro {
  --gut: 1.15rem;
  display: flex;
  flex-direction: column;
  min-height: 100%;
  text-align: center;
}

.isec {
  padding: 0 var(--gut);
  box-sizing: border-box;
}

/* `hidden` HAS TO KEEP WINNING. The UA sheet's [hidden] { display: none } is a
   type selector's worth of specificity, so every `display: grid` and
   `display: block` in this file quietly outranks it -- which showed the numbers
   row as "0 skills / 0 puzzles / --" before curriculum.json landed, and left
   "I already have an account" on screen on a deployment with no accounts at
   all. Two elements here start hidden and are revealed from JS; this is what
   makes that mean what it says. */
.intro [hidden] { display: none !important; }

/* Every button on this screen is a real <a href>, so that the launch points
   work with no JavaScript at all. That costs one rule: the anchor colour in
   styles.css is `:root:not([data-theme="light"]) a`, which is (0,1,1) and beats
   .btn's (0,1,0) -- so without this the primary call to action came out with
   brand-blue text on a brand-blue button. */
.intro a.btn,
.intro a.ipick,
.intro a.intro-skip { text-decoration: none; }
.intro a.btn {
  display: block;
  box-sizing: border-box;
  text-align: center;
  color: #fff;
}

/* ---------- skip ----------
   Sticky and first, because "skippable from the first pixel" is the difference
   between an intro and a wall. The bar itself takes no pointer events so it
   never eats a tap meant for the hero underneath it. */
.intro-topbar {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  justify-content: flex-end;
  padding: .5rem .7rem .1rem;
  pointer-events: none;
}
.intro-skip {
  pointer-events: auto;
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  color: var(--dim);
  /* Nearly opaque. It used to be 72%, which was fine over a hero and is not
     fine now: the landing is four screens long, so this pill floats over cards
     and body copy for most of the page rather than over a gradient. */
  background: color-mix(in srgb, var(--card) 92%, transparent);
  border: 0;
  border-radius: 999px;
  padding: .4rem .85rem;
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: transform .12s ease, color .12s ease;
}
.intro-skip:hover { color: var(--ink); }
.intro-skip:active { transform: scale(.94); }

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

.ihero {
  flex: 1 0 auto;
  /* CLIPS THE GLOW. The glow is deliberately wider than the board, and at 360
     and 390 that put 12-22px of it past the edge of the scroll pane -- clipped
     by the pane's own overflow-x, but overflowing all the same, and one more
     decoration would have turned that into a horizontal scrollbar. Clipped here
     instead, at the section that owns it. The gradient is near-transparent by
     the time it reaches this edge, so nothing visible is cut. */
  overflow: clip;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: .55rem;
  padding-top: .2rem;
  padding-bottom: 1.6rem;
  /* ONE SCREEN, less the sticky skip bar above it. The hero is content-sized
     without this, which on a tall phone left the facts row already half visible
     under the call to action -- so the fold landed in the middle of a section
     and the scroll cue pointed at something you could already read. The shell
     is 100dvh and the tab bar is hidden on this screen, so the pane and the
     viewport are the same height. */
  min-height: calc(100dvh - 2.8rem);
  box-sizing: border-box;
}

.ieyebrow {
  margin: 0;
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--brand-lt);
}
:root[data-theme="light"] .ieyebrow { color: var(--brand); }

.ihero h1 {
  margin: 0 0 .35rem;
  font-size: clamp(1.5rem, 7.2vw, 2.1rem);
  line-height: 1.12;
  letter-spacing: -.03em;
  text-wrap: balance;
}

/* ---------- the board ----------
   Sized off the SHORTER of the width and the height. A square that is only
   width-constrained is what pushes the call to action off the bottom of a small
   phone, and the CTA is the point of the screen. */
.iboard-wrap {
  position: relative;
  width: min(19rem, 78vw, 34dvh);
  aspect-ratio: 1 / 1;
  margin: .35rem auto .1rem;
  transition: opacity .3s ease;
}
.iboard-wrap.resetting { opacity: 0; }

.iboard {
  position: relative;
  z-index: 1;
  border-radius: .7rem;
  /* A hairline and a lift so the board reads as an object on the page rather
     than a texture printed on it. */
  box-shadow: 0 0 0 1px rgb(255 255 255 / .07), 0 18px 44px rgb(0 0 0 / .5);
}
:root[data-theme="light"] .iboard {
  box-shadow: 0 0 0 1px rgb(20 22 31 / .08), 0 14px 34px rgb(60 66 120 / .18);
}

/* The one piece of coloured light on the screen, and it is behind the thing
   the visitor is supposed to look at. Pure paint: no blur filter, because a
   blurred layer this size costs a repaint on every scrolled frame on a phone. */
.iboard-glow {
  position: absolute;
  inset: -22%;
  z-index: 0;
  border-radius: 50%;
  background:
    radial-gradient(50% 50% at 50% 45%, rgb(91 108 255 / .40), transparent 70%),
    radial-gradient(45% 45% at 72% 78%, rgb(255 95 162 / .22), transparent 70%);
  animation: iglow 9s ease-in-out infinite;
  pointer-events: none;
}
:root[data-theme="light"] .iboard-glow { opacity: .55; }
@keyframes iglow {
  0%, 100% { opacity: .8; transform: scale(1); }
  50%      { opacity: 1;  transform: scale(1.05); }
}

.icap {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  line-height: 1.3;
}
.icap-move {
  font-family: var(--mono);
  font-size: .92rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  min-height: 1.2em;
  transition: color .2s ease;
}
.icap-move.mate { color: var(--crown); }
.icap-credit {
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--dim);
}

/* THE STATIC BOARD. index.html carries a real position as inline SVG so the
   page is neither a wall of text to a crawler nor a blank square on a slow
   connection. JavaScript replaces it with the live board -- Board() clears its
   container, so there is no frame where both are on screen -- and for a visitor
   who asked for reduced motion it is simply left alone, because it is already
   the exact frame that was going to be frozen. */
.iboard-svg {
  display: block;
  width: 100%;
  height: auto;
  border-radius: .7rem;
}

/* THE LEDE. New with the static landing and the most load-bearing paragraph on
   the page for anything that reads rather than looks: it is the only place the
   offer is stated in a full sentence. Capped in measure because a 60-character
   line is the readable one and the shell is 34rem wide. */
.ilede {
  margin: .2rem auto 0;
  max-width: 30rem;
  font-size: .93rem;
  line-height: 1.55;
  color: var(--dim);
  text-wrap: pretty;
}
.ilede b { color: var(--ink); font-weight: 700; }

.ihero-cta { max-width: 20rem; margin-top: .7rem; }

/* ---------- START, OR CONTINUE ----------
 *
 * Somebody with a twelve day streak must not be sold the product they already
 * use. Both labels are in the HTML and CSS picks one, because the alternative --
 * deciding it in JavaScript -- means rendering the wrong one first and swapping
 * it, and a call to action that changes its mind in front of the reader is
 * worse than either label.
 *
 * The attribute is set by index.html's boot script from localStorage before the
 * first paint, so there is no flash. --rfx-streak is set the same way and is a
 * whole quoted phrase rather than a number, so the plural and the separator are
 * decided where the number is and this rule cannot render " · 1 days streak". */
.icta-back { display: none; }
/* THE STREAK RIDES ON THE HERO BUTTON ONLY. It is on the page twice -- once at
   the top and once at the bottom -- and a number that appears twice reads as a
   template rather than as something the page knows about you. The one at the
   top is the one that has to land. */
.ihero-cta .icta-back::after {
  content: var(--rfx-streak, "");
  font-weight: 600;
  opacity: .78;
}
:root[data-rfx="returning"] .icta-new { display: none; }
:root[data-rfx="returning"] .icta-back { display: inline; }

.icue {
  color: var(--dim);
  opacity: .7;
  margin-top: .2rem;
}
.icue svg { width: 1.5rem; height: 1.5rem; display: block; }
.icue.in { animation: icue 2.1s ease-in-out infinite; }
@keyframes icue {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(5px); }
}

/* ---------- the numbers ---------- */

.ifacts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .5rem;
  padding-top: 1.2rem;
  padding-bottom: 1.2rem;
}
.ifact {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--rule);
  padding: .85rem .3rem .75rem;
  display: flex;
  flex-direction: column;
  gap: .1rem;
}
.ifact b {
  font-size: clamp(1.15rem, 5.4vw, 1.5rem);
  font-weight: 700;
  letter-spacing: -.02em;
  /* Tabular figures so a count-up cannot change the width of the cell it is
     counting inside, which would relayout three cards on every frame. */
  font-variant-numeric: tabular-nums;
  color: var(--brand-lt);
}
:root[data-theme="light"] .ifact b { color: var(--brand); }
.ifact span { font-size: .72rem; color: var(--dim); font-weight: 600; }

/* ---------- how a lesson works ---------- */

.ihow { padding-top: 1.4rem; }
.intro h2 {
  margin: 0 0 1rem;
  font-size: 1.25rem;
  letter-spacing: -.02em;
}

.isteps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .6rem;
}
.istep {
  position: relative;
  text-align: left;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--rule);
  padding: .95rem 1.1rem .95rem 3.1rem;
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
.istep-n {
  position: absolute;
  left: .95rem;
  top: .95rem;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--brand);
  color: #fff;
  font-size: .8rem;
  font-weight: 700;
}
.istep b { font-size: .98rem; }
.istep span { font-size: .84rem; line-height: 1.45; color: var(--dim); }

/* ---------- launch ---------- */

.ilaunch { padding-top: 2rem; }
.ilaunch .btn.big { max-width: 100%; }

.ipicks {
  display: flex;
  flex-direction: column;
  gap: .55rem;
  margin-top: .7rem;
}
.ipick {
  font: inherit;
  text-align: left;
  text-decoration: none;
  color: var(--ink);
  background: var(--card);
  border: 0;
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--rule);
  padding: .8rem .95rem;
  display: grid;
  grid-template-columns: 2rem 1fr;
  align-items: center;
  gap: .8rem;
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease;
}
/* One of the picks is a real <a> (the rating test is its own document), and the
   link colour made that one card's heading brand-blue while the buttons beside
   it were ink -- which read as the odd one out rather than as the same control. */
a.ipick, a.ipick b { color: var(--ink); }
.ipick:hover { box-shadow: inset 0 0 0 1px var(--brand); }
.ipick:active { transform: scale(.985); }
.ipick-glyph {
  font-size: 1.5rem;
  line-height: 1;
  text-align: center;
  color: var(--brand-lt);
}
:root[data-theme="light"] .ipick-glyph { color: var(--brand); }
.ipick-body { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.ipick-body b { font-size: .95rem; }
.ipick-body span { font-size: .8rem; color: var(--dim); line-height: 1.35; }

.ilinky {
  display: block;
  width: 100%;
  margin-top: 1rem;
  background: none;
  border: 0;
  font: inherit;
  font-size: .85rem;
  color: var(--dim);
  text-decoration: underline;
  cursor: pointer;
  padding: .75rem .5rem;
}
.ilinky:hover { color: var(--ink); }

/* ---------- what is in the course ----------
 *
 * NEW, AND THE REASON IS SEARCH RATHER THAN DESIGN. Before this the landing had
 * three numbers and no nouns: nothing on it said "back-rank mate", "Sicilian",
 * "rook endgame". Sixteen unit names and their blurbs, baked out of
 * curriculum.json, are the strongest paragraph of real subject matter this page
 * can carry -- and unlike a keyword list they are true, they are the product,
 * and they change when the course does.
 *
 * A list rather than cards. Sixteen bordered boxes is a wall; sixteen rows with
 * a rule between them is a table of contents, which is what this is. */
.iwhat { padding-top: 2rem; }

.isub {
  margin: -.4rem 0 1.1rem;
  font-size: .88rem;
  line-height: 1.5;
  color: var(--dim);
  text-wrap: pretty;
}

.itrack {
  margin: 1.5rem 0 .5rem;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--brand-lt);
  text-align: left;
}
:root[data-theme="light"] .itrack { color: var(--brand); }
.itrack:first-of-type { margin-top: 0; }

.iunits {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: inset 0 0 0 1px var(--rule);
  overflow: hidden;
}
.iunit {
  display: grid;
  gap: .1rem;
  padding: .7rem .95rem .75rem;
  border-top: 1px solid var(--rule);
}
.iunit:first-child { border-top: 0; }
.iunit b { font-size: .92rem; }
.iunit-blurb { font-size: .82rem; line-height: 1.4; color: var(--dim); }
.iunit-n {
  font-size: .72rem;
  font-weight: 600;
  color: var(--dim);
  opacity: .8;
  font-variant-numeric: tabular-nums;
}

/* ---------- the written half of the site ----------
 *
 * These links live in index.html's footer too, but that footer only renders on
 * the Tools tab -- so on the one page whose authority they most need to inherit
 * they were invisible. Here they are a real, visible section on a real screen,
 * which is the difference between a link a reader can use and a link that is
 * merely in the markup. Resolved against the filesystem at build time, so a
 * page another script has not built yet is not offered as a dead one. */
.ireads { padding-top: 2rem; }

.ireadgrid {
  display: grid;
  gap: .5rem;
  text-align: left;
}
.iread {
  display: grid;
  gap: .1rem;
  padding: .7rem .9rem;
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: inset 0 0 0 1px var(--rule);
  text-decoration: none;
  color: var(--ink);
  transition: box-shadow .12s ease;
}
.iread:hover { box-shadow: inset 0 0 0 1px var(--brand); }
/* Same fight as a.ipick above, and it has to be won the same way: styles.css's
   `:root:not([data-theme="light"]) a` is (0,2,1) and outranks a plain class, so
   without this every heading in this grid came out link-blue while the blurb
   under it was dim -- twelve cards that read as twelve links rather than as
   twelve cards. Matching specificity, and this sheet loads later. */
.intro a.iread,
.intro a.iread b { color: var(--ink); }
.iread b { font-size: .9rem; }
.iread span { font-size: .79rem; line-height: 1.35; color: var(--dim); }

/* The Cburnett credit. CC BY-SA 3.0 attaches to any page that RENDERS the set,
   and this page renders it twice over -- once as the static SVG and once as the
   live board. It is small and at the bottom, which is a reasonable form for the
   medium, but it is not optional and it must not be dropped in a restyle. */
.icredit {
  margin: 2rem 0 0;
  padding: 0 var(--gut);
  font-size: .72rem;
  line-height: 1.5;
  color: var(--dim);
  opacity: .85;
}

.intro-tail { height: 3rem; flex: 0 0 auto; }

/* ---------- arriving ----------
   One class, one transition, staggered by a custom property the markup sets.
   Translate only -- nothing here moves a box, so a reveal never invalidates
   layout for the section under it.

   IT IS `.ireveal`, NOT `.reveal`, AND IT IS SCOPED UNDER `.intro`.
   compete.css already owns `.reveal` (the duel opponent card) and every
   stylesheet in this app is loaded on every route, so the first draft of this
   file silently inherited `display:flex; min-height:calc(100dvh - 18rem)` onto
   all twenty revealing elements: the page came out 10,699px tall with the call
   to action 2,796px down it. Nothing errored and every rule was valid -- it
   just rendered a different design. Prefix anything added here.

   IT IS OPT-IN, NOT OPT-OUT, AND THAT IS THE MOST IMPORTANT LINE IN THIS FILE.
   The landing is now real HTML served to everybody, including a crawler and
   anybody with scripting off -- and `opacity: 0` on twenty elements with
   nothing left to remove it renders as a blank page. So hiding is gated on
   `data-js`, which index.html's boot script sets before the first paint and
   drops again after 2.5 seconds unless app.js or intro.js has claimed it. Three
   consequences, all of them wanted:

     no JavaScript      nothing is ever hidden; the finished page, immediately
     JavaScript         hidden, then revealed as each section is scrolled to
     JavaScript, broken  hidden for 2.5s, then the finished page

   The third is the one worth having. A failed deploy costs the animation
   instead of costing the homepage. */
:root[data-js="on"] .intro .ireveal {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
  transition:
    opacity .5s ease,
    transform .55s cubic-bezier(.21, .9, .3, 1.05);
  transition-delay: calc(var(--i, 0) * 70ms);
}
:root[data-js="on"] .intro .ireveal.in {
  opacity: 1;
  transform: none;
}

/* ---------- the narrowest phone ----------
   At 320x720 the lede runs to eight lines and pushed the scroll cue off the
   bottom, so the one screen that has to say "there is more below this" did not.
   Measured rather than guessed: the call to action stays inside the viewport at
   every width from 320 up either way, this buys back the cue. */
@media (max-width: 22.5rem) {
  .ilede { font-size: .86rem; line-height: 1.48; }
  .ihero { gap: .42rem; padding-bottom: 1.1rem; }
  .ihero-cta { margin-top: .5rem; }
}

/* And a SHORT one. The board already sizes off 34dvh, so the thing that
   overflows a 568px-tall screen is the lede -- eight lines of it.

   MEASURED, at 320x568, 320x720, 360x640 and 390x844. Without this the call to
   action itself was 28px below the fold at 568; with it the button is inside
   the viewport at every one of them and the scroll cue is inside at every one
   but 568, where it lands 29px under. That is the right trade on the smallest
   screen still shipping: the button is the thing that has to be seen, and a
   page that plainly continues does not depend on the chevron to say so. */
@media (max-height: 40rem) {
  .ilede { font-size: .82rem; line-height: 1.42; }
  .ihero { gap: .3rem; padding-bottom: .8rem; }
  .ihero h1 { font-size: clamp(1.35rem, 6.4vw, 1.8rem); }
  .ihero-cta { margin-top: .35rem; }
  .icap-credit { font-size: .68rem; }
}

/* ---------- wider than a phone ----------
   Still one column: the shell is capped, so this is only ever a roomier phone
   layout. The extra width goes to the picks grid rather than to bigger type. */
@media (min-width: 30rem) {
  .intro { --gut: 1.6rem; }
  .ipicks { display: grid; grid-template-columns: 1fr 1fr; }
  .ihero h1 { font-size: 2.15rem; }
}

/* ---------- desktop ----------
   At 64rem styles.css turns the shell into a rail plus a content column, and
   the intro hides the tab bar -- which left the rail empty and the whole intro
   sitting off-centre in the right-hand two thirds of a 1440 screen. A landing
   page has no navigation to sit beside, so it takes the whole pane.

   Written against the shell rather than against my own classes, which is the
   one place this file reaches outside itself. It is gated on `.intro` being on
   the page, so no other screen can see it, and if that grid is ever restyled
   the rule simply stops applying rather than breaking anything. */
@media (min-width: 64rem) {
  body:has(.intro) #app { grid-column: 1 / -1; }
  .intro { max-width: 34rem; margin: 0 auto; width: 100%; }
}

/* ---------- reduced motion ----------
   Not a softening -- an off switch. Everything renders in its final state, the
   board shows one still position (intro.js picks the frame), and there is no
   loop, no pulse and no cue. */
/* SPECIFICITY IS LOAD-BEARING IN THESE TWO BLOCKS. The rule above is now
   `:root[data-js="on"] .intro .ireveal`, which is four classes' worth. The media
   query below matches it exactly and wins on source order; the .still block adds
   one class and wins on specificity. Weaken either selector and reduced motion
   silently stops being honoured, with nothing on screen to say so. */
@media (prefers-reduced-motion: reduce) {
  :root[data-js="on"] .intro .ireveal { opacity: 1; transform: none; transition: none; }
  .intro .iboard-glow { animation: none; }
  .intro .icue { display: none; }
}
:root[data-js="on"] .intro.still .ireveal { opacity: 1; transform: none; transition: none; }
.intro.still .iboard-glow { animation: none; }
.intro.still .icue { display: none; }
