@font-face {
  font-family: 'Quicksand';
  src: url('assets/fonts/Quicksand-VariableFont_wght.ttf') format('truetype');
  font-weight: 300 700;
  font-display: swap;
}

* {
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: #fff;
  color: #222;
  font-family: 'Quicksand', system-ui, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

.page {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px 20px 80px 20px;
  box-sizing: border-box;
}

.page h1 {
  font-size: 2rem;
  margin: 0 0 0.25rem 0;
}

.page h2 {
  font-size: 1.1rem;
  font-weight: 500;
  margin: 0 0 1.5rem 0;
  opacity: 0.65;
}

.placeholder {
  opacity: 0.5;
  font-style: italic;
}

/* Top bar (used on inner pages) */
.top-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 20px;
  border-bottom: 1px solid #eee;
}

.top-bar .back {
  font-size: 0.95rem;
  padding: 6px 10px;
  border: 1px solid #ddd;
  border-radius: 6px;
}

.top-bar .crumb {
  font-size: 0.95rem;
  opacity: 0.7;
}

/* Contents list */
.contents-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.contents-item {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  background: #fafafa;
}

.contents-item .num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: #e9e9e9;
  font-weight: 600;
  font-size: 0.95rem;
}

.contents-item .label {
  font-size: 1.05rem;
}

.contents-item.intro .num {
  background: #d8e7ff;
}

/* Unit hub: activities + practice sections */
.unit-section {
  margin-top: 24px;
}

.unit-section h3 {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 8px 0;
  opacity: 0.8;
}

.unit-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.unit-list .empty {
  padding: 12px 14px;
  border: 1px dashed #ddd;
  border-radius: 6px;
  font-size: 0.9rem;
  opacity: 0.5;
  font-style: italic;
}

.unit-list a {
  display: block;
  padding: 12px 14px;
  border: 1px solid #e5e5e5;
  border-radius: 6px;
  background: #fafafa;
  font-size: 0.95rem;
}

/* Unit-select landing page: the book cover and the contents list, as two
   views of ONE document. They share the main theme song, and navigating
   between separate pages would destroy and rebuild the audio element, heard
   as a gap in the music. Swapping a class on the body costs nothing.
   Only one view is mounted at a time; the cover's own styles are in
   index.html, since no other page needs them. */
.select-body {
  height: 100%;
  overflow: hidden;
  background: #0b1a24;   /* shows only before the sky paints */
}

/* The scene (sky, landscape, cast) is shared: both views sit on the same
   living backdrop, and only the cover's panels and lockup come and go.

   Both wrappers are display: contents, so every layer stays in the ONE root
   stacking context and the z-indexes set on the cover keep working across
   them. That is what lets Poppy and Patch (z-index 8) overlap the cover's
   panels (4) and lockup (7); boxing either wrapper into its own stacking
   context would flatten the cast behind them. */
.scene-view { display: contents; }
.cover-chrome { display: none; }
.view-cover .cover-chrome { display: contents; }

/* The contents view fills the window and clears the two fixed sidebars, so
   the rows never sit under them. z-index clears every scene layer (the
   highest is the dog and cat at 9 on squarer screens). */
.select-view {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 10;
  padding: 0 var(--fp-sb);
  box-sizing: border-box;
}
.view-select .select-view { display: flex; }

/* The cover carries no navigation chrome: there is nowhere above it to go
   Back to, no Home but itself, and no page to step Prev/Next through. What it
   does keep is the pair a user needs before they have started anything: Close
   and Settings, as TWO BUTTONS in the corner of the artwork, not as a bar.

   The bar is right everywhere else, where it frames a working page. On the
   cover it is a slab of dark laid over a painting, so here the bar itself is
   dismantled: no background, no full height, no width of its own. Each button
   keeps a dark chip behind it, because the icons are white line art and the
   sky behind them is not. */
.view-cover .fp-sidebar.left { display: none; }
.view-cover .fp-nav-handle,
.view-cover .fp-sidebar.right .fp-cw,
.view-cover .fp-sidebar.right .fp-zoom { display: none; }

.view-cover .fp-sidebar.right {
  background: none;
  width: auto;
  bottom: auto;
  top: 1.8vh;
  right: 1.8vh;
  padding: 0;
  gap: 1.1vh;
}
.view-cover .fp-sidebar.right .fp-sidebar-btn {
  width: var(--fp-sb);
  height: var(--fp-sb);
  padding: 16%;
  background: rgba(20, 20, 20, 0.55);
  border-radius: 24%;
}
.view-cover .fp-sidebar.right .fp-sidebar-btn:hover  { background: rgba(20, 20, 20, 0.72); }
.view-cover .fp-sidebar.right .fp-sidebar-btn:active { background: rgba(20, 20, 20, 0.72); transform: scale(0.92); }

/* --- Unit-select backdrop ----------------------------------------------
   shared scene (in index.html)  ->  black veil  ->  rows
   ----------------------------------------------------------------------- */

/* Veil, so the buttons read clearly over the artwork. */
.bg-veil {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 2;
}

/* Scrollable unit index: one row per unit, the unit button followed by its
   phoneme cards (moved here from the unit menu), so every unit's phonemes
   are visible right on the contents page. */
.unit-select {
  position: relative;
  z-index: 3;            /* above the veil */
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  padding: 4vh 3vh 6vh;
  box-sizing: border-box;
}

/* Scrollbar: slim, dark and rounded, with a light edge so it reads clearly
   over the veil. */
.unit-select::-webkit-scrollbar { width: 1.6vh; }
.unit-select::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.3);
  border-radius: 0.8vh;
}
.unit-select::-webkit-scrollbar-thumb {
  background: rgba(20, 20, 20, 0.9);
  border: 0.25vh solid rgba(255, 255, 255, 0.7);
  border-radius: 0.8vh;
}

.unit-select h1 {
  margin: 0;
  font-size: 4.5vh;
  font-weight: 700;
}

/* Teaching Guide. Built like a unit tile's banner (thick white ring, pill
   radius, drop shadow, white bold text) but smaller and in a neutral slate, so
   it reads as the same family without competing with the coloured units. */
.intro-btn {
  /* First entry in the scrolling list, so it scrolls away with the units. */
  align-self: flex-start;
  padding: 1.5vh 3vh;
  border: 0.55vh solid #fff;
  border-radius: 6vh;
  background: #4a6f8a;
  box-shadow: 0 0.6vh 1.4vh rgba(0, 0, 0, 0.3);
  color: #fff;
  font-size: 2.4vh;
  font-weight: 700;
  line-height: 1;
  text-decoration: none;
}

.unit-listing {
  /* One unit for every row metric. 1vh normally; on windows too narrow for
     the widest row (unit 4: pill 45.3 + gap 3 + 7 cards 105 + gaps 9.6 +
     panel padding 4 = ~167) it shrinks, so the whole row (pill, cards, gaps,
     panel) scales down together instead of wrapping or squeezing cards. */
  --u: min(1vh, calc((100vw - 2 * var(--fp-sb) - 8vh) / 167));
  max-width: calc(167 * var(--u));   /* don't stretch on ultrawide; sit centred */
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: calc(4 * var(--u));
}

/* Each unit's row sits on its own white panel. */
.unit-entry {
  display: flex;
  align-items: center;
  gap: calc(3 * var(--u));
  padding: calc(2 * var(--u));
  background: rgba(255, 255, 255, 0.7);
  /* Left side fully rounded (half the panel's 25.9u height), echoing the
     badge circle; the right side keeps the soft corner. */
  border-radius: calc(13 * var(--u)) calc(2 * var(--u)) calc(2 * var(--u)) calc(13 * var(--u));
}

/* Cards flow after the tile in a single line; narrow windows scale the whole
   row via --u, so nothing ever wraps or shrinks out of step. */
.unit-cards {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  /* Every row is as wide as the widest one (unit 4, seven cards), so a row of
     six left the slack in a lump at the right-hand end. Spread instead: the
     cards run edge to edge on every row and the slack goes into the spaces
     between them. The gap below is now the minimum, not the actual. */
  justify-content: space-between;
  gap: calc(1.6 * var(--u));
}

/* White rounded phoneme cards, ported from the unit-intro page at the same
   on-screen scale: there a card was 11.25cqw of a stage 141.45vh wide, i.e.
   15.9vh; held a touch under so a 7-card row fits beside the tile at 16:9.
   All inner metrics are percentages of the card's own width (via
   container-type: inline-size), exactly as on the original card. */
.ph-card {
  flex: 0 0 auto;
  width: calc(15 * var(--u, 1vh));
  aspect-ratio: 24 / 35;        /* the original 240 x 350 card */
  container-type: inline-size;  /* inner metrics key off the card's width */
  background: #fff;
  /* Horizontal / vertical radii given separately so the corner stays
     circular on a non-square box. 31.1px on 240 x 350. */
  border-radius: 12.96% / 8.89%;
  box-shadow: 0 0.25vh 0.62vh rgba(0, 0, 0, 0.28);
  cursor: pointer;
}

.ph-card-in {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 12.44cqw 8cqw 16.97cqw;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
}

/* Phonicon, centred in the upper part of the card.

   The width-and-height pair here matters: the app-wide --phscale
   normalisation further down assumes a box whose HEIGHT is the limit, so that
   every icon lands at one shared unit scale and taller art overflows upward.
   A box fixed by its width does the opposite: `object-fit: contain` then lets
   each icon's own aspect decide its height, and the --phscale factor multiplies
   that instead of correcting it. That is what left /p/, /i/, /a/, /l/ and /d/
   short while /ch/ towered over its row. Running the box out to the card's
   full width (back over .ph-card-in's 8cqw side padding) leaves the height as
   the limit for all but the widest handful, which is what the normalisation
   wants. The same reasoning used to be spelled out on .letterform for unit 5's
   vowels; every card now gets it. */
.ph-card-icon {
  flex: 1 1 auto;
  min-height: 0;
  width: calc(100% + 16cqw);
  margin-left: -8cqw;
  margin-right: -8cqw;
  object-fit: contain;
  margin-bottom: 5.66cqw;
}

/* One of unit 5's letterforms carries about 8% of clear space above and below
   inside its own file, so at a matched box height its letter still lands
   short. See LETTERFORM / PADDED in index.html. */
.ph-card-icon.letterform.padded { transform: scale(1.087); }

/* The two th mouths are set large by eye everywhere else (see the --phscale
   block below); on these tiles they keep their artwork scale so they fit. */
.ph-card-icon[src$="phonicons/hth.svg"] { --phscale: 0.733; }
.ph-card-icon[src$="phonicons/sth.svg"] { --phscale: 0.734; }

/* Phoneme label, bottom-centred in each card (Quicksand-Bold, black) */
.ph-card-label {
  flex: 0 0 auto;
  font-weight: 700;
  font-size: 26cqw;   /* under the original 32.07, so /yoo/ and /igh/ stay
                         inside the card */
  line-height: 1;
  color: #000;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

/* Unit-intro banner: a secondary-coloured rounded banner with a big
   primary-coloured circle badge overhanging its left edge. Both carry a thick
   white ring/border and a drop shadow. All inner sizing is in container units
   (cqw/cqh) so a tile scales as one unit at any size. */
.unit-tile {
  box-sizing: border-box;
  flex: 0 0 auto;
  width: calc(45.3 * var(--u, 1vh));   /* definite w+h so container-type:size
                          can't collapse. Height matches the cards (15 x
                          35/24); width keeps the tile's original 31:15
                          proportions at that height */
  height: calc(21.9 * var(--u, 1vh));
  position: relative;
  container-type: size;
  overflow: visible;   /* let the badge overhang the banner */
}

.unit-tile .banner {
  position: absolute;
  top: 10cqh;
  bottom: 10cqh;
  left: 22cqw;
  right: 0;
  box-sizing: border-box;
  border: 5cqh solid #fff;
  border-radius: 20cqh;
  background: #fafafa;
  box-shadow: 0 5cqh 12cqh rgba(0, 0, 0, 0.22);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  line-height: 1;
  padding: 0 5cqw 0 26cqw;   /* clear the overhanging badge on the left */
  text-align: center;
  z-index: 1;
}

.unit-tile .kicker {
  font-weight: 700;
  font-size: 15cqh;
  letter-spacing: 0.08em;
  transform: translateY(3px);   /* nudge 'Unit' down a touch */
}

.unit-tile .kicker.tight {   /* Three / Seven sit higher above the smaller word */
  transform: none;
}

.unit-tile .word {
  font-weight: 800;
  font-size: 32cqh;
}

.unit-tile .word.fm {
  font-size: 18cqh;
  white-space: nowrap;
}

.unit-tile .word.long {   /* "Three" / "Seven" — trimmed to fit the banner */
  font-size: 26cqh;
}

.unit-tile .badge {
  position: absolute;
  left: 2cqw;   /* nudged right off the tile edge; banner and text unmoved */
  top: 50%;
  transform: translateY(-50%);
  height: 88cqh;
  aspect-ratio: 1;
  box-sizing: border-box;
  border-radius: 50%;
  border: 5cqh solid #fff;
  background: #e9e9e9;
  box-shadow: 0 5cqh 12cqh rgba(0, 0, 0, 0.24);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #666;
  font-weight: 800;
  font-size: 48cqh;
  line-height: 1;
  z-index: 2;
}

/* Coloured unit tiles: white text on the primary badge + secondary banner. */
.unit-tile[data-unit] .badge,
.unit-tile[data-unit] .banner { color: #fff; }

/* Tap feedback for phoneme icons, app-wide: a quick grow-and-settle while the
   phoneme plays. FPCards.bindPlay applies this, so every phonicon that speaks
   also pops; pages that bind their own listener add the class themselves. */
.fp-pop { animation: fp-pop 0.25s ease-out; }
@keyframes fp-pop {
  0%, 100% { transform: scale(var(--phscale, 1)); }
  40%      { transform: scale(calc(var(--phscale, 1) * 1.2)); }
}

@media (prefers-reduced-motion: reduce) {
  .fp-pop { animation: none; }
}

/* ---- Phonicon height normalisation, app-wide ---------------------------
   Every phonicon SVG is cropped to its artwork, and one SVG unit is the
   same real size across the whole set: a standard character body is
   129.77 units tall, and icons whose art genuinely carries more (the
   pump handle on f, the train steam on ch, the sil-pair arcs) just have
   taller viewBoxes. Pages size every icon into one fixed-height box,
   which shrank exactly those characters. The factor below is each icon's
   viewBox height / 129.77, so its body renders the same height as its
   neighbours and the extra art overflows upward, as the book prints it.
   It rides --phscale rather than a bare transform so the fp-pop
   keyframes above keep it while the tap animation plays. */
img[src*="phonicons/"]:not([src*="phonicons/pwr"]) {
  transform: scale(var(--phscale, 1));
  transform-origin: bottom center;
}
img[src$="phonicons/air.svg"]       { --phscale: 0.936; }
img[src$="phonicons/aitch.svg"]     { --phscale: 1.071; }
img[src$="phonicons/ar.svg"]        { --phscale: 1.094; }
img[src$="phonicons/bee.svg"]       { --phscale: 1.071; }
img[src$="phonicons/ch.svg"]        { --phscale: 1.253; }
img[src$="phonicons/d.svg"]         { --phscale: 1.2; }     /* by eye */
img[src$="phonicons/dee.svg"]       { --phscale: 1.071; }
img[src$="phonicons/doubleyoo.svg"] { --phscale: 1.071; }
img[src$="phonicons/eff.svg"]       { --phscale: 1.071; }
img[src$="phonicons/el.svg"]        { --phscale: 1.071; }
img[src$="phonicons/em.svg"]        { --phscale: 1.071; }
img[src$="phonicons/en.svg"]        { --phscale: 1.071; }
img[src$="phonicons/er.svg"]        { --phscale: 1.029; }
img[src$="phonicons/ess.svg"]       { --phscale: 1.094; }
img[src$="phonicons/ex.svg"]        { --phscale: 1.079; }
img[src$="phonicons/f.svg"]         { --phscale: 1.364; }
img[src$="phonicons/hth.svg"]       { --phscale: 1; }       /* by eye */
img[src$="phonicons/jay.svg"]       { --phscale: 1.087; }
img[src$="phonicons/jee.svg"]       { --phscale: 1.094; }
img[src$="phonicons/kay.svg"]       { --phscale: 1.071; }
img[src$="phonicons/n.svg"]         { --phscale: 1.159; }
img[src$="phonicons/ng.svg"]        { --phscale: 1.157; }
img[src$="phonicons/o.svg"]         { --phscale: 1.069; }
img[src$="phonicons/oi.svg"]        { --phscale: 1.236; }
img[src$="phonicons/ool.svg"]       { --phscale: 0.806; }
img[src$="phonicons/oos.svg"]       { --phscale: 0.903; }
img[src$="phonicons/ou.svg"]        { --phscale: 1.065; }
img[src$="phonicons/ow.svg"]        { --phscale: 1.065; }
img[src$="phonicons/pee.svg"]       { --phscale: 1.071; }
img[src$="phonicons/queue.svg"]     { --phscale: 1.333; }
img[src$="phonicons/r.svg"]         { --phscale: 1.086; }   /* by eye */
img[src$="phonicons/sch.svg"]       { --phscale: 1.394; }
img[src$="phonicons/see.svg"]       { --phscale: 1.094; }
img[src$="phonicons/sh.svg"]        { --phscale: 0.969; }
img[src$="phonicons/silai.svg"]     { --phscale: 1.464; }
img[src$="phonicons/silee.svg"]     { --phscale: 1.464; }
img[src$="phonicons/siligh.svg"]    { --phscale: 1.464; }
img[src$="phonicons/siloa.svg"]     { --phscale: 1.464; }
img[src$="phonicons/silyoo.svg"]    { --phscale: 1.456; }
img[src$="phonicons/sth.svg"]       { --phscale: 1; }       /* by eye */
img[src$="phonicons/tee.svg"]       { --phscale: 1.071; }
img[src$="phonicons/u.svg"]         { --phscale: 1.394; }
img[src$="phonicons/vee.svg"]       { --phscale: 1.071; }
img[src$="phonicons/wo.svg"]        { --phscale: 1; }       /* by eye */
img[src$="phonicons/why.svg"]       { --phscale: 1.071; }
img[src$="phonicons/y.svg"]         { --phscale: 1.158; }
img[src$="phonicons/zee.svg"]       { --phscale: 1.071; }
img[src$="phonicons/zh.svg"]        { --phscale: 0.956; }

/* Ultrawide. Past 2:1 the activity stage stops growing and centres itself,
   so a row of cards does not stretch the whole way across a very wide
   monitor. Every activity page names its stage #stage, and the id beats the
   `max-width: none` each page sets in its own long-landscape breakpoint. */
@media (min-aspect-ratio: 2 / 1) {
  #stage { max-width: 200vh; margin-left: auto; margin-right: auto; }
}

/* Diagonal stripe background using the active unit's theme colours.
   Apply to any element (commonly body) with class="striped-bg".
   --theme-primary / --theme-secondary are set by shared/theme.js.
   Falls back to greys if no theme is loaded. */
.striped-bg {
  background: repeating-linear-gradient(
    45deg,
    #fff 0 15px,
    var(--theme-primary, #ddd) 15px 30px,
    #fff 30px 45px,
    var(--theme-secondary, #bbb) 45px 60px
  );
}

/* ---- Hollow letters, app-wide ------------------------------------------
   Outlined type with an opaque fill: the colouring words, Do And Say's
   graphemes, the Phoneme Intro symbol. The obvious way to draw it is
   `-webkit-text-stroke` with `paint-order: stroke`, which puts the stroke
   under the fill so only its outer half shows.

   THE ELECTRON SHELL CANNOT DO THAT. apps/fastphonics-electron pins
   Electron 22.3.27, i.e. Chromium 108, which answers
   CSS.supports('paint-order', 'stroke') with true and then ignores the
   property on HTML text. The centred stroke therefore paints straight over
   the fill: the counters of a / e / p close up, Quicksand's overlapping
   contours (the bowl of a p against its stem, the crossbar of a t) show as
   double-drawn internal joins, and a word reads as a black blob. The same
   bug was found and fixed the same way in dinosaurdiner-ng (123f253); the
   phone and any modern browser were never affected, which is why it only
   ever showed up in the shell.

   So the outline is a ring of eight zero-blur text-shadows instead. Shadows
   are painted BEHIND the glyph in every engine, no property support needed,
   and eight copies dilate it to an octagon whose inradius is cos(22.5°) =
   0.92 of the radius: under a tenth of a pixel of thickness variation at
   these sizes. Proven byte-comparable against the paint-order rendering
   under Electron 43 and correct under Electron 22.

   --fp-hollow-r is the radius. The arithmetic conversion is HALF the centred
   stroke it replaces, because half is all a paint-order stroke ever showed
   outside the glyph - but the pages then take 20% off that, because a ring
   that measures the same as the old stroke READS heavier than one: it is a
   solid dilation of the glyph rather than a stroke the fill bites back into,
   so nothing softens its outer edge. Each page therefore carries 0.8x its
   halved figure, and the comment there records both numbers.
   --fp-hollow-c is its colour. Both may be any unit or colour, cq units
   included; set them on the element and add the class.

   The fill is just the element's `color`, so a page colours the letters in
   by setting that - which is also what fp-painter.js writes. */
.fp-hollow {
  --fph-r: var(--fp-hollow-r, 2px);
  --fph-c: var(--fp-hollow-c, #000);
  --fph-d: calc(var(--fph-r) * 0.70711);   /* the diagonals: cos/sin 45° */
  text-shadow:
    var(--fph-r) 0 0 var(--fph-c),
    var(--fph-d) var(--fph-d) 0 var(--fph-c),
    0 var(--fph-r) 0 var(--fph-c),
    calc(var(--fph-d) * -1) var(--fph-d) 0 var(--fph-c),
    calc(var(--fph-r) * -1) 0 0 var(--fph-c),
    calc(var(--fph-d) * -1) calc(var(--fph-d) * -1) 0 var(--fph-c),
    0 calc(var(--fph-r) * -1) 0 var(--fph-c),
    var(--fph-d) calc(var(--fph-d) * -1) 0 var(--fph-c);
}
