/* css/styles.css - shell styles: reset, header, footer, buttons, typography.
   Lesson list and reader live in lessons-home.css; phone tweaks in mobile.css. */

/* 1. Global reset & page shell */

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Same reason as the lesson pages: the root owns the page colour so the body's
   own background stays an element background the header's blur can sample. */
html {
  background-color: var(--bg-primary);
  /* The page ends where the footer ends. No rubber band past it, and no
     pull-to-refresh above it: the plate at the foot is the edge of the sheet,
     not a thing the sheet slides off. Chrome, Firefox and Safari 16+ honour
     this; iOS Safari's bounce on the root is stopped in js/app.js. */
  overscroll-behavior: none;
}

body {
  overscroll-behavior: none;
}

body {
  font-family: var(--font-body);
  background-color: var(--bg-primary);
  color: var(--text-primary);
  direction: inherit;
  text-align: start;
  line-height: 1.6;
  overflow-x: clip;
  position: relative;
  min-height: 100vh;
}

/* The paper only crossfades when the theme is actually being changed.

   Left unconditional, this transition belongs to the element rather than to
   the event, so it runs for any restyle that lands on it — including the first
   one after a stylesheet or a webfont arrives, which is a page opening, not a
   theme change. That is why a lesson opened while the site was already dark
   still played a light-to-dark fade every time it was entered: nobody had
   touched the switch, the document had simply been mounted.

   `is-theme-wiping` is set by the switch and by nothing else, in this document
   and in the lesson's (see toggleTheme / setTheme), and it is set for the
   length of the ink sweep — which is longer than this transition. So the fade
   happens on a real change and only on a real change, and mounting, remounting
   and navigating are silent. Every other animation on the page is untouched. */

html.is-theme-wiping body {
  transition: background-color var(--transition-normal), color var(--transition-normal);
}

.container {
  width: 100%;
  max-width: 1200px;
  margin-left: auto;
  margin-right: auto;
  padding-left: var(--spacing-lg);
  padding-right: var(--spacing-lg);
}

a {
  color: inherit;
  text-decoration: none;
}

/* Decorative manga grid + halftone background layers */

.manga-grid-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: auto;
  bottom: auto;
  width: 100%;
  height: 100%;
  border: none;
  box-sizing: border-box;
  background-image: 
    linear-gradient(to right, rgba(0, 0, 0, 0.012) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.012) 1px, transparent 1px);
  background-size: 80px 80px;
  pointer-events: none;
  z-index: 10;
}

[data-theme="dark"] .manga-grid-overlay {
  background-image: 
    linear-gradient(to right, rgba(255, 255, 255, 0.006) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.006) 1px, transparent 1px);
  background-size: 80px 80px;
}

/* The paper is the same chart the hero is drawn on, printed faint: depth
   contours instead of a halftone dot. It is one sheet from the top of the
   page to the bottom — the hero is the deep water on it, the lesson list is
   the same sheet dry.

   Carried as a mask rather than as a picture, so the ink comes from the
   theme's own token and one asset serves both. The tile is periodic, built
   from sines on integer frequencies, so the contours cross the seam and
   carry on. */
  /* Carried inline rather than as a file. Chrome refuses to load an external
     SVG as a mask across file:// origins — it fails outright,
which left the
     page with no texture at all for anyone opening index.html off the disk,
a way this site is explicitly built to be read. A data URI is same-origin
     everywhere,
so the one asset the whole page is textured with cannot go
     missing. It is 24KB of path data,
and it is why the tile is drawn at
     integer precision. */
.halftone-bg {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--bg-dots);
  -webkit-mask-image: var(--chart-mask);
  mask-image: var(--chart-mask);
  -webkit-mask-size: var(--chart-tile, 560px);
  mask-size: var(--chart-tile, 560px);
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  opacity: 1;
  pointer-events: none;
  z-index: 0;
}

/* A tile sized for a desktop reads as one enormous curve on a phone — at that
   width you only ever see a corner of it. Shrinking the tile keeps the same
   density of line on screen at every size. */
@media (max-width: 720px) {
  :root { --chart-tile: 340px; }
}

/* 2. Typography */

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-display);
  font-weight: 800;
  color: var(--text-primary);
  line-height: 1.25;
}

/* 3. Header */

/* The header is three things stacked, and each owns exactly one job.

   .main-header is the sticky rail. Its height never changes — that is the
   whole reason the morph below is safe. A sticky element still occupies its
   original slot in the flow, so growing it while it is stuck would re-flow
   everything under it and the page would nudge on the first scroll. The rail
   therefore keeps a constant padding-block forever and animates only its
   padding-inline, which nothing beneath it can feel.

   The object it carries is the island — css/island.css. The rail owns
   position and height; the island owns everything you can see. */
.main-header {
  position: sticky;
  /* The rail holds the height; the island floats in it.

     The island is `position: absolute` so that opening a drawer cannot push
     the page (see the geometry contract in css/island.css) — which means it
     contributes no height of its own, and without this line the rail
     collapsed to its own padding. --header-h is a fact the hero pulls itself
     up by, so the rail has to be exactly that tall whether or not anything
     inside it is in the flow. Border-box, so the padding below is inside the
     number rather than added to it. */
  height: var(--header-h);
  top: 0;
  z-index: 100;
  /* Uneven on purpose: the gap above is what makes the bar read as lifted off
     the top of the screen rather than hung from it, and the smaller gap below
     leaves room for the hard shadow it drops. Constant in both states. */
  padding-block: calc(env(safe-area-inset-top, 0px) + var(--header-pad-top))
                 var(--header-pad-bottom);
  /* The one thing that moves horizontally. 0 -> 2rem a side is the bar pulling
     in from the edges of the window; it is the same 4rem of total inset the
     reference draws, expressed as the rail's own gutter so the bar can simply
     be width:100% of whatever it is given. */
  padding-inline: 0;
  background: transparent;
  border: 0;
  transition: padding-inline var(--dur-morph) var(--ease-morph);
}

.main-header.is-scrolled {
  padding-inline: 2rem;
}

/* The slab, the row, the wordmark and the actions strip that used to live
   here are gone: css/island.css owns all four now, as one object. What is
   left of the header in this file is the rail it is carried on — a sticky
   band whose height never changes, because --header-h is a fact the hero
   pulls itself up by and a bar that grew while stuck would nudge the page on
   the first scroll. The rail's one moving part is the gutter beside it.

   The `is-scrolled` class is still set by initHeaderScroll and still means
   what it meant. What it does with it changed: the plate no longer draws
   itself in out of nothing, because an instrument the whole site is arranged
   around cannot start the page as nothing. It changes material instead —
   solid ground to glass — which says the page is moving behind it. */

/* Nothing to morph into if the page cannot move. */
@media (prefers-reduced-motion: reduce) {
  .main-header {
    transition: none;
  }
}


/* 5. Hero badge (used by the lesson list) */
.hero-badge-comic {
  font-family: var(--font-comic);
  background-color: var(--accent-orange);
  color: HSL(224, 30%, 12%);
  padding: 0.35rem 0.8rem;
  border: 2px solid var(--border-color);
  box-shadow: var(--shadow-sm);
  display: inline-block;
  font-size: 0.9rem;
  font-weight: 800;
  transform: rotate(-2deg);
  margin-bottom: var(--spacing-md);
}

/* 6. Section titles */
.section-title-wrapper {
  text-align: center;
  margin-bottom: var(--spacing-xxl);
  position: relative;
}

.section-subtitle-comic {
  font-family: var(--font-comic);
  color: var(--primary);
  /* 1.2rem, not 1.1: at 800 weight this clears 18.66px, which is where a
     browser stops treating it as body copy. The brand orange reads 3.8:1 on
     the paper — comfortably past the 3:1 large text is held to, and short of
     the 4.5:1 it would owe at 17.6px. The alternative was dulling the one
     colour this label exists to carry. */
  font-size: 1.2rem;
  font-weight: 800;
  text-transform: uppercase;
  margin-bottom: var(--spacing-xs);
}

.section-title-comic {
  font-size: 2.2rem;
  font-weight: 900;
  display: inline-block;
  position: relative;
}

.section-title-comic::after {
  content: '';
  position: absolute;
  bottom: -6px;
  left: 5%;
  width: 90%;
  height: 4px;
  background-color: var(--border-color);
}

/* 7. Lesson badge in the reader header */
.study-volume-badge {
  background-color: transparent;
  color: white;
  font-family: var(--font-comic);
  font-size: 1.05rem;
  padding: 0.5rem 1.1rem;
  border-radius: 0;
  transform: rotate(-3deg);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 50' preserveAspectRatio='none'><path d='M 10,25 C 15,12 30,6 60,5 C 90,4 110,12 115,22 C 120,32 105,42 80,45 C 55,48 25,45 10,40 C -5,35 5,38 10,25 Z' fill='%23dc2626'/><circle cx='112' cy='38' r='3' fill='%23dc2626'/><circle cx='8' cy='15' r='2.5' fill='%23dc2626'/><circle cx='95' cy='12' r='2' fill='%23dc2626'/></svg>");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: center;
  text-shadow: 1px 1px 0px rgba(0,0,0,0.5);
  display: inline-block;
}

/* 8. Footer — see css/footer.css, which owns the colophon entire. */

/* 9. Responsive */

@media (max-width: 600px) {
.container {
    padding-left: 12px;
    padding-right: 12px;
  }

}

/* 10b. Which view a load opens on

   The reader is hidden by default and the list is not, because that is what
   most loads are. A refresh inside a lesson is the exception, and the inline
   script in index.html's <head> names it before anything has been painted —
   see the comment there. Without this the browser painted the home page, and
   the router animated the reader in on top of it a beat later.

   showView writes `display` inline on both panels the moment the router is
   awake, and inline styles outrank these, so this decides the first paint and
   nothing after it. */

/* The home page is what most loads are, so it is the one the markup shows and
   the other two start hidden. A load that is not a home load says so on <html>
   before any script runs, and these swap which panel is painted first. */
#subjectView,
#lessonReaderView {
  display: none;
}

html[data-boot-view="reader"] #lessonReaderView,
html[data-boot-view="subject"] #subjectView {
  display: block;
}

html[data-boot-view="reader"] #homeView,
html[data-boot-view="subject"] #homeView {
  display: none;
}

/* 11. Print stylesheet */

@media print {
body {
    background: #fff !important;
    color: #000 !important;
  }

/* The island goes. A lesson printed from the shell is the lesson, and the
   instrument that took you to it is furniture on paper. */
.main-header,
.manga-grid-overlay,
.halftone-bg,
#themeToggle {
    display: none !important;
  }

.view-panel {
    display: block !important;
  }
}

/* The wordmark used to be set in the sea's own light ink at the top of the
   home page, because the bar had no ground there and the hero was behind it.
   The island always has a ground, so the type is always on paper and there is
   no second palette for it to wear. One less state, and the one that was
   hardest to keep legible: it painted white on white the moment the site grew
   a subject sheet, and had to be scoped back to the index by hand. */

/* 12. Edge tab — the back / refresh control parked on the side of the screen */

.edge-back {
  position: fixed;
  z-index: 120; /* above the sticky header, which the tab can be parked over */
  /* Where it was last left. A fixed element with no `top` falls back to its
     static position, and this button is the last thing in the body — so until
     initEdgeBack ran, the tab was painted at the very bottom of the screen and
     jumped up to its place a moment later. The inline script in index.html's
     <head> publishes the stored offset here; the fallback is the same default
     initEdgeBack uses when there is nothing stored. */
  top: var(--edge-tab-top, 72%);
  translate: 0 -50%;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 4rem;
  padding: 0;
  color: var(--text-primary);
  background: var(--accent-yellow-fill);
  border: 3px solid var(--border-color);
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* A half-disc: flat against the edge it sits on, round on the side it faces. */
.edge-back[data-edge="start"] {
  inset-inline-start: 0;
  border-inline-start: 0;
  border-radius: 0 2rem 2rem 0;
}

[dir="rtl"] .edge-back[data-edge="start"] {
  border-radius: 2rem 0 0 2rem;
}

.edge-back[data-edge="end"] {
  inset-inline-end: 0;
  border-inline-end: 0;
  border-radius: 2rem 0 0 2rem;
}

[dir="rtl"] .edge-back[data-edge="end"] {
  border-radius: 0 2rem 2rem 0;
}

.edge-back.is-dragging {
  background: var(--primary);
  color: var(--text-inverse);
}

@media (hover: hover) {
  .edge-back:hover {
    background: var(--primary);
    color: var(--text-inverse);
  }
}

.edge-back.is-dragging {
  cursor: grabbing;
  transition: none;
}

/* Out of the way of the hero on a phone. Parked at 72% of the screen, the
   tab stood exactly on the headline's second line on a narrow screen and
   took a bite out of it — and on the index, at the top, it is a refresh
   button nobody has any use for yet. So it is tucked back into its edge until
   the page has been scrolled; the slide is short and it comes back the same
   way. Set from initEdgeBack, which is the one place that knows the view. */
.edge-back {
  transition: opacity 220ms ease, transform 260ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.edge-back.is-tucked {
  opacity: 0;
  pointer-events: none;
  transform: translateX(-100%);
}

html[dir="rtl"][data-edge-tab="start"] .edge-back.is-tucked,
html:not([dir="rtl"])[data-edge-tab="end"] .edge-back.is-tucked {
  transform: translateX(100%);
}

/* It sits whole on the edge, and it used to not.

   On anything with a pointer it rested tucked 42% of its width behind the side
   of the screen and slid out when aimed at. The intent was to keep it off the
   text; what it actually produced was a control with a bite taken out of it,
   because a half-disc cut in half again is not a tab that is tucked away — it
   is a shape that looks broken. And it only looked that way on a desktop: a
   touch screen has no hover to slide it out with, so there it had always stood
   at its full size, which is what the shape was drawn for.

   Now both do. The tab is complete on every device and is kept out of the way
   by weight instead of by geometry: it rests a little transparent, comes up to
   full on hover or focus, and takes the same hard ink shadow every other raised
   thing on this page casts, so it reads as a tab lying on the edge rather than
   as something half-swallowed by it. */
.edge-back {
  opacity: 0.88;
}

/* Cast inward, away from the edge it is flat against — a shadow falling off
   the side of the screen is a shadow nobody can see. */
.edge-back[data-edge="start"] {
  box-shadow: var(--shadow-sm);
}

.edge-back[data-edge="end"] {
  box-shadow: var(--shadow-pop);
}

/* The same two sides, decided before the button exists.

   The rules above key off the button's own data-edge, which only initEdgeBack
   can set — until it does, the markup's value is a guess, and for anyone who
   had parked the tab on the other edge it was the wrong one. These read the
   answer off <html>, where the <head> script puts it before the first paint;
   place() keeps it in step from then on. They win on specificity, and each one
   clears the inset the other set, because a fixed element given both is
   over-constrained rather than moved. */
html[data-edge-tab="start"] .edge-back {
  inset-inline-start: 0;
  inset-inline-end: auto;
  border-inline-start: 0;
  border-inline-end: 3px solid var(--border-color);
  border-radius: 0 2rem 2rem 0;
  box-shadow: var(--shadow-sm);
}

html[dir="rtl"][data-edge-tab="start"] .edge-back {
  border-radius: 2rem 0 0 2rem;
}

html[data-edge-tab="end"] .edge-back {
  inset-inline-start: auto;
  inset-inline-end: 0;
  border-inline-start: 3px solid var(--border-color);
  border-inline-end: 0;
  border-radius: 2rem 0 0 2rem;
  box-shadow: var(--shadow-pop);
}

html[dir="rtl"][data-edge-tab="end"] .edge-back {
  border-radius: 0 2rem 2rem 0;
}

@media (hover: hover) {
  .edge-back:hover,
  .edge-back:focus-visible,
  .edge-back.is-dragging {
    opacity: 1;
  }
}

.edge-back-icon {
  width: 20px;
  height: 20px;
}

/* Chevrons are physical: back points toward the start of the reading order. */
[dir="rtl"] .edge-back-arrow {
  transform: rotate(180deg);
  transform-origin: center;
}

.edge-back .edge-back-refresh,
.edge-back.is-refresh .edge-back-arrow {
  display: none;
}

.edge-back.is-refresh .edge-back-refresh {
  display: block;
}

/* The label names the action for assistive tech; the shape carries it visually. */
.edge-back-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 12. Motion
   ==========================================================================
   Ink on paper: things land, they do not fade. The offset shadow is the
   material that moves — a control pressed travels into its own shadow, a
   panel arriving snaps its shadow out from under itself.
   ========================================================================== */

/* --- the theme wipe ------------------------------------------------------ */

/* The default cross-fade would dissolve one theme into the other. The wipe
   replaces it: the old theme holds still while the new one is revealed under
   an expanding circle, so it reads as ink spreading, not as a dissolve. */
/* The ink itself: one disc, grown from the nib on the compositor.

   This replaces a view transition. A view transition holds two snapshots of
   the whole page at device resolution and animates a clip-path on the main
   thread — on a phone that is both more memory than there is to spare and more
   work than the frame budget allows, which is why the wipe used to start,
   stutter and then jump to the end, and why the reader (running its own
   transition inside the shell's) could take the tab down with it.

   A disc costs one small layer and one transform. Nothing is snapshotted,
   nothing is re-rastered as it grows, and the page underneath re-themes while
   it is hidden — so the whole sweep plays, at full rate, on any phone. */
.theme-ink {
  position: fixed;
  top: 0;
  left: 0;
  width: 96px;
  height: 96px;
  margin: -48px 0 0 -48px;
  border-radius: 50%;
  background: var(--bg-primary);
  z-index: 2147483646;
  pointer-events: none;
  will-change: transform, opacity;
  transform: scale(0);
}

/* Screentone at the spreading edge: the ring rides out with the wipe, so the
   boundary reads as halftone breaking up rather than a hard geometric circle. */
html.is-theme-wiping::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 2147483647;
  pointer-events: none;
  background-color: var(--bg-dots);
  -webkit-mask-image: var(--chart-mask);
  mask-image: var(--chart-mask);
  -webkit-mask-size: 560px;
  mask-size: 560px;
  opacity: 0;
  animation: theme-screentone var(--dur-focal) var(--ease-out);
}

@keyframes theme-screentone {
  0% { opacity: 0; }
  35% { opacity: 0.5; }
  100% { opacity: 0; }
}

.python-lesson-card,
.edge-back {
  transition:
    translate var(--dur-tap) var(--ease-out),
    box-shadow var(--dur-tap) var(--ease-out),
    opacity var(--dur-state) ease,
    background-color var(--dur-state) ease,
    color var(--dur-state) ease,
    border-color var(--dur-state) ease;
}

/* The press travels into the shadow *from where the tab is centred*: the -50%
   is what puts the tab's middle on --edge-tab-top, and a plain `2px 2px` here
   threw it away, so every press dropped the tab by half its own height. That
   drop is what put the tab under the finger by 34px for the whole of a drag,
   and what made it leap back up the moment the finger let go. */
.edge-back:active {
  translate: 2px calc(-50% + 2px);
  box-shadow: 0 0 0 var(--shadow-color) !important;
}

/* Held, the tab is being carried, not pressed: no press travel, no transition
   chasing the pointer. */
.edge-back.is-dragging,
.edge-back.is-dragging:active {
  translate: 0 -50%;
  transition: none;
}

.python-lesson-card:active {
  translate: 3px 3px;
  box-shadow: 1px 1px 0 var(--shadow-color) !important;
}

/* --- arrival ------------------------------------------------------------- */

/* The sheet wipe.

   Changing screens used to be a hard swap: the old panel went display:none,
   the new one appeared and was scaled up from 94% — which on the reader meant
   the entire iframe growing out of the bottom of the page like a curtain going
   up, with a white frame and the footer showing through in between. A view
   change is now one physical thing: a sheet of the chart's own deep water
   slides across the page, the screen is changed while it is covered, and the
   sheet carries on off the far side to uncover what is there now. Forward
   travel enters from the reading edge (the right, in RTL) and leaves left;
   going back runs the other way, so the direction says which way you went.

   The slab is twice the viewport wide and only ever moves on transform, on the
   compositor: nothing underneath is snapshotted or re-rastered, and the swap
   itself happens on a frame nobody can see. The lean is a manga panel gutter;
   the brass rules are its edges; the contour chart on it is the hero's own. */
.page-wipe {
  position: fixed;
  inset: 0;
  z-index: 2147483645; /* under the theme ink, over everything else */
  overflow: hidden;
  /* Never in the way: a tap during the sweep reaches whatever is under it,
     and a second navigation replaces the one waiting under the sheet
     (sheetWipe in js/app.js). The edge tab in particular must stay grabbable
     the instant you land somewhere. */
  pointer-events: none;
}

.page-wipe-slab {
  position: absolute;
  top: -25vh;
  bottom: -25vh;
  left: -50vw;
  width: 200vw;
  background-color: #06192c; /* --sea-abyss: the same ground in both themes */
  background-image: linear-gradient(180deg, #0a2947 0%, #06192c 58%, #041322 100%);
  border-inline: 7px solid var(--accent-yellow);
  transform: translateX(100%) skewX(-12deg);
  will-change: transform;
  contain: strict;
}

/* The chart printed on the water: the hero's contour lines, the theme
   wipe's screentone, on the sheet itself so it travels with it. */
.page-wipe-slab::after {
  content: '';
  position: absolute;
  inset: 0;
  background-color: rgba(94, 186, 240, 0.16);
  -webkit-mask-image: var(--chart-mask);
  mask-image: var(--chart-mask);
  -webkit-mask-size: 560px;
  mask-size: 560px;
}

/* The lean is a stroke, and a stroke has a thickness: a hairline of paper
   between the brass and the ink, the way a panel border is inked with a gap. */
.page-wipe-slab::before {
  content: '';
  position: absolute;
  inset: 0 -14px;
  border-inline: 3px solid var(--bg-primary);
  pointer-events: none;
}

/* A panel is stamped: it lands slightly hard, and its shadow is thrown clear
   a beat later, the way a stamp leaves the page before the ink settles.

   Only the pieces on a page are stamped now — never the page itself. The panel
   is uncovered by the sheet, whole; the cards on it arrive under the reveal in
   reading order, so the sheet seems to be leaving them behind. */
@keyframes stamp-in {
  0% {
    opacity: 0;
    scale: 0.94;
    translate: 0 10px;
  }
  100% {
    opacity: 1;
    scale: 1;
    translate: 0 0;
  }
}

.is-entering .section-title-wrapper,
.is-entering .sheet-head {
  animation: stamp-in var(--dur-view) var(--ease-out) 120ms backwards;
}

.is-entering .python-lesson-card {
  animation: stamp-in var(--dur-view) var(--ease-stamp) backwards;
}

.is-entering .python-lesson-card:nth-child(1) { animation-delay: 200ms; }
.is-entering .python-lesson-card:nth-child(2) { animation-delay: 280ms; }
.is-entering .python-lesson-card:nth-child(3) { animation-delay: 360ms; }
.is-entering .python-lesson-card:nth-child(4) { animation-delay: 440ms; }

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

/* Fewer and gentler, not silent: colour and state still change so an action is
   still acknowledged — only the travel is removed. */
@media (prefers-reduced-motion: reduce) {
  .is-entering,
  .is-entering * {
    animation: none !important;
  }

  .edge-back:active {
    translate: 0 -50%;
  }

  .python-lesson-card:active {
    translate: none;
  }

  html.is-theme-wiping::after {
    animation: none;
  }
}
