/* The narrative page. It shares web-ui/style.css's palette on purpose: the
   playground and the local browser UI run the same interpreter and draw the
   same cascade, so they should not look like two projects.

   The one structural difference is the reading measure. web-ui/ is an
   application laid out in panels; this is a page someone reads on a phone
   before deciding whether to press anything, so prose is capped at 34rem and
   nothing is allowed to push the document sideways. */

:root {
  --bg: #05070a;
  --panel: #0b0f14;
  --edge: #16202a;
  --green: #00ff41;
  --dim: #0d7a2a;
  --head: #ccffcc;
  --text: #c8d4cd;
  --muted: #5d6f66;
  --white: #e8f0ea;
  --amber: #d68a2a;
  /* JetBrains Mono first, but the fallbacks are load-bearing rather than
     decorative: the subset shipped here is basic latin, and this page prints
     half-width katakana in every example. Those characters fall through to
     Menlo/SF Mono, which do carry them. Dropping the tail would render the
     glyph face as a row of tofu. */
  --mono: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
  --prose: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --measure: 34rem;
  /* The desktop width. ONE token, deliberately: on a wide screen the reading
     column and the bands below it share an edge, and a page whose paragraphs
     start 128px inside the examples they introduce looks like two documents
     shuffled together. A second token set to the same number would drift the
     first time one of them moved, so prose and bands read this one.

     A phone still reads at 34rem. The cost of the desktop setting is real and
     worth stating: measured by counting rendered lines across the six long
     paragraphs, 90rem runs about 151 characters against the 45–75 typography
     calls comfortable. Every width considered — 48rem measured 93 — was
     already past that range, so the choice was never comfortable versus not.
     It was alignment against line length, and alignment won. */
  --page: 90rem;
}

/* ---- the typeface ---- */

/* Self-hosted, not fetched from a font CDN. Three reasons and they all matter:
   the Content-Security-Policy in index.html allows `font-src 'self'` only, a
   third-party font request tells someone else who reads this page, and a file
   served from the same origin arrives without a second DNS lookup and TLS
   handshake.

   Basic-latin subsets, ~31KB each. site/fonts/OFL.txt is the licence and has
   to stay beside them: JetBrains Mono is SIL Open Font License 1.1, which
   permits bundling and requires the notice travel with the font. */
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  /* `swap`, deliberately. The page is a document first: text in a fallback
     face beats no text at all, and the reflow when the real face lands is a
     smaller cost than a blank headline. */
  font-display: swap;
  src: url("fonts/jetbrains-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/jetbrains-mono-700.woff2") format("woff2");
}
/* Italic exists for one phrase — <i>The Matrix</i> in the headline, set at
   700. Without a real 700 italic the browser slants the upright, and a
   synthesised oblique in a monospace face at display size is obvious.

   700 only. A 400 italic would be dead weight: nothing on this page renders
   italic inside a monospace element except that headline, so the browser
   never requested the file. Should one appear, it synthesises from 400
   upright, which is the right failure for text nobody has written yet. */
@font-face {
  font-family: "JetBrains Mono";
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/jetbrains-mono-700-italic.woff2") format("woff2");
}

* { box-sizing: border-box; }

html { background: var(--bg); }

body {
  margin: 0;
  padding: 0 1.25rem 4rem;
  /* Transparent, not --bg: the wall is a fixed canvas behind the document,
     and a painted body would hide it. The colour moves up to <html> so the
     page is still the right colour before the canvas has drawn a frame, and
     for a reader whose JavaScript never ran. */
  background: transparent;
  color: var(--text);
  font: 17px/1.65 var(--prose);
  /* A long unbroken token in prose must wrap rather than widen the page. */
  overflow-wrap: break-word;
}

/* ---- the reading column ---- */

/* The reading column keeps its measure; the band joins it until wide. */
.masthead,
.prose,
.band,
.tabs {
  max-width: var(--measure);
  margin: 0 auto;
}

.examples {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.75rem;
  /* `auto`, not 0: this rule comes after the one that centres .band and has
     the same specificity, so a plain `0` here silently won and left the
     examples flush against the padding while the prose above them stayed
     centred. Invisible on a phone, where the band is wider than the screen
     anyway, and plain to see the moment a desktop reader picks Mobile. */
  margin: 0 auto 2rem;
}

.masthead { padding: 3.5rem 0 2.5rem; }

.eyebrow {
  margin: 0 0 1.25rem;
  color: var(--dim);
  font: 11px/1 var(--mono);
  letter-spacing: 0.28em;
}

/* The headline is set in the same face the code is. The page argues that the
   film's falling characters were never a language and that this one is, so a
   headline typed in the language's own face states that before a word of it
   is read — where a book serif would have quietly argued the opposite.

   The three values are neither defaults nor arbitrary. JetBrains Mono sets
   wide, so 1.7rem holds the apparent size the old 1.9rem sans had; 700
   because a monospace goes limp at display size below it, and this line is
   the loudest thing on the page; -0.02em because monospace sidebearings are
   drawn for columns of code, not for a sentence. */
h1 {
  margin: 0;
  color: var(--head);
  font-family: var(--mono);
  /* Fluid rather than fixed: this is the first thing on the page now, and a
     1.7rem line that filled a narrow column looks like a caption once the
     column runs to the window's edge. */
  font-size: clamp(2rem, 6.4vw, 4rem);
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: 0.01em;
  text-align: center;
  text-transform: uppercase;
  /* The glow is the one piece of set dressing in the type. Two shadows: a
     tight one for the phosphor edge, a wide one for the bloom. */
  text-shadow: 0 0 20px rgba(0, 255, 65, 0.55), 0 0 52px rgba(0, 255, 65, 0.26);
}

.premise {
  margin: 1rem auto 0;
  max-width: 34rem;
  color: var(--muted);
  font-size: 0.95rem;
  text-align: center;
}

h2 {
  margin: 3rem 0 1rem;
  color: var(--green);
  font: 12px/1.4 var(--mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

p { margin: 0 0 1rem; }

i { color: inherit; }

b { color: var(--white); font-weight: 600; }

a {
  color: var(--green);
  text-decoration-color: var(--dim);
  text-underline-offset: 3px;
}
a:hover { color: var(--head); }

/* The one sentence the whole project reduces to. */
.thesis {
  margin: 1.5rem 0;
  padding-left: 1rem;
  border-left: 2px solid var(--dim);
  color: var(--white);
  font-size: 1.15rem;
}

/* ---- code ---- */

code {
  color: var(--white);
  font-family: var(--mono);
  font-size: 0.85em;
}

pre {
  margin: 0;
  padding: 0.9rem 1rem;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 4px;
  color: var(--white);
  font: 13px/1.7 var(--mono);
  /* Code is the one thing that may exceed the measure. It scrolls inside its
     own box so the document never does. */
  overflow-x: auto;
  tab-size: 2;
}

/* ---- two faces ---- */

.faces {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: 1fr;
  margin: 0 0 1.5rem;
}

.faces figure { margin: 0; min-width: 0; }

.faces figcaption {
  margin-bottom: 0.4rem;
  color: var(--muted);
  font: 10px/1.4 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.glyph { color: var(--green); }

#cascade-face,
#editor-face,
.face-toggle {
  margin-top: 0.75rem;
}

#editor-glyph,
.example-glyph {
  margin-top: 0.5rem;
  color: var(--green);
  white-space: pre-wrap;
}

@media (min-width: 40rem) {
  .faces { grid-template-columns: 1fr 1fr; }
}

/* ---- the examples ---- */

.example { margin: 0; }

/* What a person asked for, in their own words, above what came back. */
.request {
  margin: 0 0 0.5rem;
  padding-left: 0.85rem;
  border-left: 2px solid var(--edge);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 13px;
}

.example .output {
  margin-top: 0.4rem;
  border-color: var(--dim);
  color: var(--green);
}

/* Two examples print nothing, correctly: they declare rather than announce.
   This note takes the place of the output block rather than sitting beside an
   empty one — a blank box on a page that promises "watch it run" reads as a
   fault, and it isn't one. */
.declares {
  margin: 0.5rem 0 0;
  color: var(--muted);
  font-size: 0.9rem;
}

/* ---- the tabs ---- */

.tabs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.55rem;
  margin: 0 auto 1.9rem;
  padding: 0;
  background: none;
  border: 0;
}

.tab {
  margin: 0;
  padding: 0.6rem 1rem;
  border: 1px solid var(--dim);
  border-radius: 2px;
  /* Not transparent: a chip sits over the wall, and unreadable navigation is
     a worse trade than a little less atmosphere. */
  background: rgba(5, 10, 7, 0.72);
  color: var(--head);
  font: 11px/1.2 var(--mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
}
.tab:hover { color: #fff; border-color: var(--green); box-shadow: 0 0 12px rgba(0, 255, 65, 0.28); }
.tab[aria-selected="true"] {
  color: #fff;
  border-color: var(--green);
  box-shadow: 0 0 14px rgba(0, 255, 65, 0.32);
}

/* ---- the transliterator ---- */

.translit-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  margin: 1rem 0;
}
.translit-grid textarea { min-height: 8rem; }

.translit-caption {
  margin: 0.5rem 0 0;
  color: var(--muted);
  font-size: 0.9rem;
}

#translit-table {
  margin-top: 0.5rem;
  max-height: 16rem;
  overflow-y: auto;
  white-space: pre-wrap;
}

/* ---- the playground ---- */

#playground { padding-top: 1rem; }

label {
  display: block;
  margin: 1.5rem 0 0.4rem;
  color: var(--muted);
  font: 10px/1.4 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

input, textarea {
  display: block;
  width: 100%;
  padding: 0.6rem 0.75rem;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 4px;
  color: var(--white);
  font: 14px/1.7 var(--mono);
  tab-size: 2;
}
input { line-height: 1.4; }
textarea { resize: vertical; }
/* Both, and the textarea half is load-bearing. `input::placeholder` does not
   match a <textarea>, so #program-input's hint fell back to the browser
   default — measured at rgb(117,117,117) against this page's rgb(93,111,102),
   lighter than every other placeholder on the page. In a box that must be
   filled or the program fails, a hint that reads as content is a trap, and it
   caught the first person to use it. */
input::placeholder,
textarea::placeholder { color: var(--muted); }
input:focus, textarea:focus {
  outline: none;
  border-color: var(--dim);
}

button {
  margin-top: 0.75rem;
  padding: 0.55rem 1rem;
  background: transparent;
  border: 1px solid var(--dim);
  border-radius: 4px;
  color: var(--green);
  font: 13px/1.2 var(--mono);
  cursor: pointer;
}
button:hover { background: #08120c; color: var(--head); }
button:disabled { border-color: var(--edge); color: var(--muted); cursor: default; }

#boot {
  width: 100%;
  padding: 0.9rem 1rem;
  font-size: 14px;
}

/* Misses and diagnostics. Never transliterated — an error is the moment a
   reader's fluency has failed, and glyphs are the worst possible response. */
#miss {
  margin: 0.75rem 0 0;
  padding: 0.6rem 0.75rem;
  background: var(--panel);
  border-left: 2px solid var(--amber);
  color: var(--amber);
  font: 13px/1.5 var(--mono);
  white-space: pre-wrap;
}

/* Collapsed by default: the pane already carries Scribe, an editor and an
   input box, and a reader who does not write Python should not pay for it
   in vertical space. */
#python-panel summary { cursor: pointer; }
#python-source { width: 100%; }

/* The input box. Deliberately short: it holds the lines a reader chooses to
   supply up front, and anything the program still wants is asked for one
   line at a time in #answer-row below. Short because filling it in advance
   is now the shortcut rather than the only way in. */
#program-input {
  min-height: 3.5rem;
}

/* The interactive answer row. Present only while a program is waiting, so
   it must read as part of the run rather than as another form field the
   reader was supposed to fill in beforehand. */
#answer-row {
  margin-top: 0.75rem;
  padding: 0.75rem;
  background: var(--panel);
  border-left: 2px solid var(--green);
  border-radius: 0 4px 4px 0;
}

#answer-prompt {
  margin: 0 0 0.5rem;
  color: var(--white);
  font: 13px/1.5 var(--mono);
  white-space: pre-wrap;
}

#cascade {
  display: block;
  width: 100%;
  max-width: 640px;
  /* An explicit ratio, NOT `height: auto`.
     `Cascade.resize()` writes canvas.width/height from getBoundingClientRect(),
     and `height: auto` derives the rendered height from those same attributes —
     so the ResizeObserver fed its own output back in and the box crept from
     16:9 to a 1:1 square, thinning the wall over a taller area. web-ui avoids
     this by giving the canvas a definite height inside a sized flex parent;
     this page has no such parent, so the ratio is stated here instead. */
  aspect-ratio: 16 / 9;
  margin-top: 1.5rem;
  background: #000;
  border: 1px solid var(--edge);
  border-radius: 4px;
}

/* ---- the Operator panel ---- */

#operator-panel {
  margin-top: 2rem;
  padding: 0.9rem 1rem;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 4px;
}

#operator-panel summary {
  color: var(--muted);
  font: 12px/1.4 var(--mono);
  cursor: pointer;
}
#operator-panel summary:hover { color: var(--text); }

/* The paragraph a reader must not skim past. The plan asked for dimmer text;
   dimming is what makes a paragraph skimmable, so it keeps full reading
   contrast and the amber that this page uses nowhere except for things that
   have gone wrong or could. It is the loudest block on the page by
   construction: warm against an otherwise entirely green document. */
.warning {
  margin: 1rem 0;
  padding: 0.85rem 1rem;
  background: #120d06;
  border-left: 3px solid var(--amber);
  border-radius: 0 4px 4px 0;
  color: var(--white);
  font-size: 0.95rem;
}
.warning a { color: var(--amber); text-decoration-color: var(--amber); }
.warning a:hover { color: #f0b464; }

#api-key { margin-top: 0.5rem; }

/* ---- the intro ---- */

/* Hidden by default and shown only by the attribute intro.js sets. The
   default direction matters: if the script never runs, never loads, or throws
   before it decides, the reader gets the page rather than a black screen with
   nothing behind it. */
.intro { display: none; }

:root[data-intro="playing"] .intro {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: #000;
  opacity: 1;
  transition: opacity 900ms ease;
  cursor: pointer;
}

:root[data-intro="playing"] .intro.gone { opacity: 0; pointer-events: none; }

/* The document must not scroll behind an overlay that covers it. */
:root[data-intro="playing"] body { overflow: hidden; }

/* A phosphor screen is brightest at the centre and falls off at the glass. */
:root[data-intro="playing"] .intro::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 35%, rgba(0, 0, 0, 0.75) 100%);
}

.intro-terminal {
  width: min(90vw, 44rem);
  padding: 0 1rem;
  font-family: var(--mono);
  font-size: clamp(1rem, 2.6vw, 1.6rem);
  line-height: 2;
  color: var(--green);
  /* The glow is the look. A CRT does not draw a hard edge. */
  text-shadow: 0 0 2px rgba(0, 255, 65, 0.9), 0 0 12px rgba(0, 255, 65, 0.45);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.intro-line { display: block; min-height: 1em; }

/* The glyph face reads brighter than the Latin it replaced — the turn should
   register as an event, not as the same line in a different alphabet. */
.intro-terminal .glyphed { color: var(--head); }

/* A block, not a bar: it is what a terminal of that era drew. */
.intro-cursor {
  display: inline-block;
  width: 0.6em;
  height: 1.05em;
  margin-left: 0.06em;
  background: var(--green);
  vertical-align: -0.16em;
  box-shadow: 0 0 8px rgba(0, 255, 65, 0.6);
  animation: intro-blink 1.06s steps(1) infinite;
}

@keyframes intro-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* Belt and braces. intro.js already declines to play under reduced motion, so
   this only matters if the intro is forced with ?intro. */
@media (prefers-reduced-motion: reduce) {
  .intro-cursor { animation: none; }
  :root[data-intro="playing"] .intro { transition: none; }
}

/* Overrides the page's button rule: this one sits on black, not on the page. */
.intro-skip {
  position: absolute;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 1;
  margin: 0;
  padding: 0.45rem 0.9rem;
  background: rgba(5, 7, 10, 0.7);
  border: 1px solid var(--edge);
  border-radius: 3px;
  color: var(--muted);
  font: 11px/1.2 var(--mono);
  cursor: pointer;
}
.intro-skip:hover { color: var(--head); border-color: var(--dim); }

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

footer { border-top: 1px solid var(--edge); margin-top: 4rem; padding-top: 0.5rem; }

.links { margin: 0 0 2rem; padding: 0; list-style: none; }
.links li { margin-bottom: 0.9rem; }

.colophon {
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.6;
}

/* ---- the layout band ---- */

:root {
  /* The layout switch. `--layout` is the single flag both the media query
     and the [data-layout] override flip; @container style() consumes it
     below, so the wide band is written once. */
  container-type: style;
  --layout: mobile;
}

/* Auto: the viewport decides, unless the reader is pinned to Mobile. */
@media (min-width: 60rem) {
  :root:not([data-layout="mobile"]) { --layout: desktop; }
}
/* Forced: Desktop wins regardless of width, Mobile loses the media query. */
:root[data-layout="desktop"] { --layout: desktop; }

/* THE wide band — written exactly once. */
@container style(--layout: desktop) {
  /* The reading column and the bands widen together to the same edge. One
     declaration rather than two, so they cannot be given different widths by
     accident. Set here rather than by rewriting --measure on :root, because
     :root is the style container itself and a container query cannot
     restyle it. */
  .masthead,
  .prose,
  .band,
  .tabs { max-width: var(--page); }
  /* The canvas fills its column now that the column is wide. The cap existed
     to stop a 640-wide drawing being stretched across a whole screen; inside
     a bounded band it only left dead space to the right of the wall.
     `aspect-ratio` above is what holds the ResizeObserver loop shut, and it
     is untouched. */
  #cascade { max-width: none; }
  .examples {
    grid-template-columns: 1fr 1fr;
    gap: 1.75rem;
  }
  .translit-grid {
    grid-template-columns: 1fr 1fr;
    gap: 1.75rem;
  }
}

#layout-switch {
  display: inline-flex;
  gap: 2px;
  margin: 0 0 1.25rem;
  padding: 2px;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 4px;
}
#layout-switch button {
  margin: 0;
  padding: 0.3rem 0.7rem;
  border: none;
  border-radius: 3px;
  font: 11px/1.2 var(--mono);
  color: var(--muted);
  background: transparent;
  cursor: pointer;
}
#layout-switch button:hover { color: var(--head); background: #08120c; }
#layout-switch button.active {
  color: var(--green);
  background: #0a1a0f;
}


/* ---------------------------------------------------------------------- */
/* The front page: one console over a wall.                                */
/*                                                                          */
/* The playground used to be two columns with the cascade beside the editor,*/
/* inside the reading measure. It is now a single column that runs close to */
/* the window's edge, with the output window on top, and the wall moved out */
/* from behind the editor to behind the whole page.                         */

#page-rain {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* Scenery must never eat a click meant for the page above it. */
  pointer-events: none;
}

/* Everything that is the document sits over the wall. Stated on the three
   landmarks rather than on `*`, so a stacking context is not created inside
   the console, where the answer row and the glyph face rely on normal flow. */
.masthead,
main,
footer { position: relative; z-index: 1; }

.masthead { text-align: center; }

/* The console. Opaque on purpose: the wall behind the page is atmosphere,
   and a wall showing through the box you are trying to type code into is
   not. --console is its own token rather than --page because this box is
   not the reading column and should not inherit its measure argument. */
:root { --console: 110rem; }

.console {
  max-width: var(--console);
  margin: 0 auto;
  padding: 1.35rem 1.5rem 1.75rem;
  background: var(--panel);
  border: 1px solid var(--dim);
  border-radius: 5px;
  box-shadow: 0 0 34px rgba(0, 0, 0, 0.85), 0 0 18px rgba(0, 255, 65, 0.10);
}

/* `label` already carries this; the output window is a canvas, which cannot
   be labelled by one. */
.field-label {
  display: block;
  margin: 0 0 0.42rem;
  color: var(--muted);
  font: 11px/1.2 var(--mono);
  letter-spacing: 0.17em;
  text-transform: uppercase;
}

.boot-note {
  max-width: 46rem;
  margin: 0.75rem auto 1.5rem;
  color: var(--muted);
  font-size: 0.85rem;
  text-align: center;
}

/* The output window. Its face toggle doubles as the window's title bar —
   one element rather than a bar plus a button inside it. */
.cascade-pane {
  /* Capped against the viewport, not the console. The console runs nearly
     the full width now, and a 16:9 canvas that wide is taller than the
     screen -- a reader would land on a title and an empty box, and have to
     scroll past a window with nothing in it yet to reach the field they
     came to type in. Bounding the WIDTH keeps the ratio intact, which is
     what stops cascade.js's ResizeObserver feeding its own output back in
     (see #cascade below). 76vh of width is 43vh of height. */
  max-width: min(100%, 76vh);
  margin: 0 auto 0.4rem;
  background: #02050a;
  border: 1px solid var(--edge);
  border-radius: 3px;
  overflow: hidden;
}
#cascade-face {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.5rem 0.7rem;
  border: 0;
  border-bottom: 1px solid var(--edge);
  border-radius: 0;
  background: #060b10;
  text-align: right;
}
#cascade-face:hover { background: #0a1410; }
#cascade {
  width: 100%;
  max-width: none;
  margin: 0;
}

/* ---- the Examples panel ---- */

.exgrid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
  margin: 1.5rem 0 0;
}

.ex {
  padding: 0.9rem 1rem 1rem;
  background: #04080a;
  border: 1px solid var(--edge);
  border-radius: 3px;
}
.ex h3 {
  margin: 0 0 0.6rem;
  color: var(--head);
  font: 700 0.82rem/1.3 var(--mono);
  letter-spacing: 0.04em;
}
.ex .source,
.ex .output {
  margin: 0 0 0.6rem;
  padding: 0.6rem 0.7rem;
  background: #02050a;
  border: 1px solid #101a20;
  border-radius: 2px;
  font: 12px/1.55 var(--mono);
  white-space: pre-wrap;
  overflow-x: auto;
}
.ex .source { color: var(--white); }
/* The output is what the interpreter printed, so it is the green one. */
.ex .output { color: var(--green); }
.ex-load { margin: 0; }

/* One column on a phone, whatever the auto-fill would have allowed. */
@media (max-width: 30rem) {
  .exgrid { grid-template-columns: 1fr; }
}
