/* =========================================================================
   PyTeach — design system
   -------------------------------------------------------------------------
   Built for 11-14 year olds: big touch targets, chunky "pressable" buttons,
   high-contrast text, one number on screen (progress) instead of a dashboard.

   Two rules hold the whole thing together:
     1. Logical properties ONLY (inline-start/end, margin-inline, ...). The
        site ships in 16 languages, 3 of them RTL — physical left/right would
        need a mirrored stylesheet. Code blocks are the sole exception and are
        pinned LTR on purpose.
     2. Every colour, radius, shadow and duration comes from a token below.
        Nothing hard-codes a hex value.
   ========================================================================= */

:root {
  /* --- palette ----------------------------------------------------------
     Game-app colours: few hues, fully saturated, each with a darker twin used
     as the solid bottom edge on buttons and tiles. Green leads because Pity is
     green — the mascot and the primary action are the same colour, so "press
     the green thing" is one idea for a 12-year-old, not two.

     Every *-deep is the pressed/edge shade of its parent. Nothing else in this
     file hard-codes a hex, so re-theming the whole app happens right here. */
  --brand:        #58cc02;   /* primary action */
  --brand-deep:   #46a302;
  --brand-soft:   #e8f9d5;

  --sun:          #ffc800;   /* rewards, streak-ish highlights */
  --sun-deep:     #e5a400;
  --grass:        #58cc02;   /* success — same green, kept as its own token
                                so "correct" can be re-tinted independently */
  --grass-deep:   #46a302;
  --coral:        #ff4b4b;   /* wrong / danger */
  --coral-deep:   #e03131;
  --sky:          #1cb0f6;   /* info, links, secondary action */
  --sky-deep:     #1899d6;
  --violet:       #a560ff;   /* islands, mascot accents */
  --violet-deep:  #8549cc;
  /* LinkedIn's own blue. Not one of ours, and deliberately not re-tinted to
     our green: this is somebody else's mark, and recolouring it makes it
     harder to recognise rather than more on-brand. */
  --linkedin:      #0a66c2;
  --linkedin-deep: #084d92;

  --ink:          #3c3c3c;
  --ink-soft:     #6b6b6b;   /* 5.33:1 on --paper; #777 was 4.48 */
  --ink-faint:    #8a8a8a;   /* 3.54:1; #afafaf was 2.19 and unreadable */

  /* Bare anchors. This was referenced once at .markdown-body's sibling rule and
     defined NOWHERE, so `color: var(--link)` fell back to the inherited ink and
     every link outside lesson prose rendered the same colour as the text around
     it — with text-decoration: none two lines below, weight was the only cue
     left. --sky-deep is the tempting choice and fails AA at 3.20:1. */
  --link:         #0f7cb8;   /* 4.57:1 on --paper */

  /* The interface face. Named so a control sitting inside a monospace box
     can ask for it back: "font: ... inherit" there inherits the CODE font,
     which is how the Persian label on the "try it" button ended up in a
     monospace fallback instead of Estedad. */
  --ui-font: "Estedad", ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  /* Text colours for tinted status surfaces. --brand-deep/--coral-deep are
     FILL colours: on their own soft backgrounds they measured 2.21:1. These
     are the ink siblings, dark on a pale tint and pale on a dark one, so a
     flash message or a failing-test reason stays readable in both themes. */
  --ink-grass:    #2f6b02;
  --ink-coral:    #a12238;
  --ink-sun:      #8a5a00;

  --paper:        #ffffff;
  --canvas:       #ffffff;   /* the page is pure white; colour comes from content */
  --canvas-deep:  #f7f7f7;
  --line:         #e5e5e5;

  /* --- the map's water --------------------------------------------------
     Only the voyage uses these, but they belong with the palette. Measured
     on --sea (L = 0.7258):
       --sea-deep  5.38:1   the rim that makes the wake visible
       --sea-line  3.48:1   the route ahead, and the phone rail
     Both clear the 3:1 WCAG 1.4.11 asks of a graphic that carries meaning.
     --wake on --sea alone is 1.07:1 — gold and pale blue sit at almost the
     same luminance — which is why the wake is drawn with a dark rim and
     never touches the water directly. */
  --sea-pale:  #e6f6ff;
  --sea:       #6cc0ec;   /* open water — deeper than the old wash */
  --sea-line:  #0a4f73;   /* the route's dashes: 4.4:1 on the water above */
  --sea-deep:  #0d5c85;
  --wake:      #ffd233;

  /* --- shape ------------------------------------------------------------ */
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 24px;
  --r-xl: 34px;
  --r-pill: 999px;

  /* Neutral, low shadows. The depth in this UI comes from the solid darker
     edge under every pressable thing, not from blur — blur on a white canvas
     just looks smudged. */
  --shadow-sm: 0 2px 5px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.09);
  --shadow-lg: 0 16px 38px rgba(0, 0, 0, 0.14);
  /* How far a pressable surface sits above its edge, and how far it travels. */
  --lift: 4px;

  /* --- motion ----------------------------------------------------------- */
  --t-fast: 0.14s;
  --t-mid: 0.26s;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);

  --nav-h: 68px;
  --measure: 74ch;
  /* Width of the centred reading column. Everything on a content page — text,
     images, code, quizzes — sits inside this and is centred in the viewport,
     so the page reads like a sheet of paper rather than a full-width wall. */
  --page: 860px;
}

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-block-size: 100vh;
  font-family: var(--ui-font);
  font-size: 1.02rem;
  line-height: 1.75;
  color: var(--ink);
  background: var(--canvas);
}

h1, h2, h3, h4 {
  line-height: 1.28;
  font-weight: 800;
  margin-block: 0 0.5em;
  letter-spacing: -0.01em;
}
h1 { font-size: clamp(1.7rem, 1.2rem + 2.2vw, 2.6rem); }
h2 { font-size: clamp(1.35rem, 1.1rem + 1.2vw, 1.85rem); }
h3 { font-size: 1.22rem; }
p  { margin-block: 0 1em; }


img { max-inline-size: 100%; height: auto; }

:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 3px;
  border-radius: 6px;
}

.container {
  inline-size: min(1120px, 100% - 2.2rem);
  margin-inline: auto;
}

/* =========================================================================
   The page sheet — a centred reading column.
   Content pages (lesson, exercise, certificate, profile, auth) wrap their
   body in .page so text, images, code and widgets all share one measure and
   sit in the middle of the viewport. Direction is inherited from <html dir>,
   so the same rules give a centred LTR page in English and a centred RTL
   page in Persian/Arabic/Urdu with no mirrored stylesheet.
   ========================================================================= */
.page {
  inline-size: min(var(--page), 100% - 2rem);
  margin-inline: auto;
  padding-block: 2rem 4rem;
}
.page-wide { --page: 1080px; }

/* Everything inside the sheet stays within the measure and centres itself. */
.page > * { margin-inline: auto; }

/* Media is centred as a block, never floated — a 13-year-old reading a lesson
   should never hunt for where the picture went. */
.page img,
.markdown-body img {
  display: block;
  margin-inline: auto;
  border-radius: var(--r-md);
}

/* Code is the one thing that must NOT follow the page direction: Python is
   left-to-right even inside an Arabic sentence. isolate stops the bidi
   algorithm from dragging brackets and operators to the wrong end. */
.page pre,
.markdown-body pre,
.page code,
.markdown-body code {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}

/* Inline code keeps its spaces. An inline element collapses runs of spaces and
   trims a lone one, so `Hello    Python` shown as code rendered `Hello Python`
   and a single space shown as code (`` ` ` ``) rendered as an empty box. A block
   <pre> is already white-space: pre; this gives inline <code> the same, and
   pre-wrap so it still wraps inside a paragraph instead of overflowing. */
.page code,
.markdown-body code {
  white-space: pre-wrap;
}

/* …but code inside a BLOCK stays `pre`. A fenced block renders as
   <pre><code>every character of the sample</code></pre>, and that inner <code>
   is matched by the rule above — which would soft-wrap a long line at the
   column edge instead of letting it scroll. Wrapping a line is a lie about the
   code a child is copying, so the block re-asserts `pre` and keeps the
   horizontal scroll (.highlight pre { overflow-x: auto } in markdown.css). */
.page pre code,
.markdown-body pre code {
  white-space: pre;
}

/* Skip link — keyboard users shouldn't tab the whole nav on every page. */
.skip-link {
  position: absolute;
  inset-block-start: -100px;
  inset-inline-start: 1rem;
  z-index: 200;
  padding: 0.7rem 1.1rem;
  background: var(--paper);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);
  font-weight: 700;
}
.skip-link:focus { inset-block-start: 0.7rem; }

/* =========================================================================
   Buttons — chunky, with a real "press" travel. The bottom edge is a solid
   darker slab, so pressing feels physical rather than a colour swap.
   ========================================================================= */
.btn {
  --btn-face: var(--brand);
  --btn-edge: var(--brand-deep);
  --btn-text: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.8rem 1.6rem;
  border: 0;
  border-radius: var(--r-md);
  background: var(--btn-face);
  /* The solid slab under the face is the whole trick: it reads as a physical
     key with a thickness, so pressing has somewhere to go. */
  box-shadow: 0 var(--lift) 0 var(--btn-edge);
  color: var(--btn-text);
  font: inherit;
  font-weight: 800;
  font-size: 0.98rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  transition: transform var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              filter var(--t-fast);
}
.btn:hover { color: var(--btn-text); filter: brightness(1.05); }
/* Travel exactly --lift so the face lands flush on its edge — any other
   number and the key looks like it sinks through the surface. */
.btn:active {
  transform: translateY(var(--lift));
  box-shadow: 0 0 0 var(--btn-edge);
}
/* Pressed wins over hover, always. Declared after every hover rule so a
   stuck :hover on a touch screen cannot keep a button floating. */
.btn:active,
.btn:hover:active { transform: translateY(var(--lift)); box-shadow: 0 0 0 var(--btn-edge); }
.btn-ghost:active,
.btn-ghost:hover:active { box-shadow: 0 0 0 var(--line); border-color: var(--line); }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: 0 var(--lift) 0 var(--btn-edge);
  filter: grayscale(0.4);
}

.btn-sun   { --btn-face: var(--sun);    --btn-edge: var(--sun-deep);    --btn-text: #4a3200; }
.btn-grass { --btn-face: var(--grass);  --btn-edge: var(--grass-deep); }
.btn-coral { --btn-face: var(--coral);  --btn-edge: var(--coral-deep); }
.btn-sky   { --btn-face: var(--sky);    --btn-edge: var(--sky-deep); }
.btn-violet{ --btn-face: var(--violet); --btn-edge: var(--violet-deep); }

/* Ghost: the same physical key, drawn as an outline. Its edge is the border
   colour so it still presses. */
.btn-ghost {
  --btn-text: var(--ink-soft);
  background: var(--paper);
  border: 2px solid var(--line);
  box-shadow: 0 var(--lift) 0 var(--line);
}
/* Hover-only, and only where hovering exists. On a touch screen :hover
   sticks after a tap, so this rule was leaving the white button blue with a
   5px edge that never sank: it is declared after .btn:active, so it won, and
   the one button on the page appeared not to respond to being pressed. */
@media (hover: hover) {
.btn-ghost:hover {
  --btn-text: var(--sky-deep);
  border-color: var(--sky);
  box-shadow: 0 var(--lift) 0 var(--sky);
  background: var(--paper);
  filter: none;
}
}

.btn-sm { padding: 0.52rem 1.05rem; font-size: 0.84rem; --lift: 3px; }
.btn-lg { padding: 1.05rem 2.2rem; font-size: 1.08rem; --lift: 5px; }
.btn-block { display: flex; inline-size: 100%; }

/* Uppercase is wrong for scripts that have no case, and Persian/Arabic/Urdu
   text gets letter-spaced apart into unreadable mush. Latin-only styling. */
:is([lang="fa"], [lang="ar"], [lang="ur"], [lang="hi"], [lang="ja"], [lang="zh"]) .btn {
  text-transform: none;
  letter-spacing: 0;
}

/* =========================================================================
   Cards & surfaces
   ========================================================================= */
.card {
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.4rem 1.5rem;
}
.card-title { margin-block-start: 0; }

.stack > * + * { margin-block-start: 1rem; }
.muted { color: var(--ink-soft); }
.tiny { font-size: 0.86rem; }

/* =========================================================================
   Header / navbar
   ========================================================================= */
.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 90;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: saturate(1.6) blur(14px);
  border-block-end: 1.5px solid var(--line);
}

.nav {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-block-size: var(--nav-h);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 900;
  font-size: 1.22rem;
  color: var(--ink);
  text-decoration: none;
  flex: none;
}
.brand:hover { color: var(--ink); }
.brand-logo {
  inline-size: 40px;
  block-size: 40px;
  /* Green, to match the badge's rim. This was a purple glow left over from an
     earlier flat mark, which under a dark green disc read as a bruise. */
  filter: drop-shadow(0 3px 9px rgba(74, 210, 119, 0.45));
  transition: transform var(--t-mid) var(--ease-bounce);
}
.brand:hover .brand-logo { transform: rotate(-8deg) scale(1.06); }
.brand-name b { color: var(--brand); }

.nav-spacer { flex: 1 1 0; min-inline-size: 0; }

.nav-links {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  /* Was flex: none, which made the bar as wide as its contents and pushed the
     whole page sideways once a staff account added two more links. It may
     shrink now, and min-inline-size: 0 lets it actually do so, because a flex
     item refuses to go below its content width without it. */
  flex: 0 1 auto;
  min-inline-size: 0;
}
.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.85rem;
  white-space: nowrap;
  border-radius: var(--r-pill);
  color: var(--ink-soft);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast);
}
.nav-link:hover { background: var(--brand-soft); color: var(--brand-deep); }
.nav-link.is-active { background: var(--brand-soft); color: var(--brand-deep); }

/* --- the single progress bar, front and centre ------------------------- */
/* Moving sheen, purely decorative. */
@keyframes sheen { from { transform: translateX(-100%); } to { transform: translateX(100%); } }


/* --- credits chip ------------------------------------------------------- */
.credits-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  /* Was as wide as a button for two characters of number. It sits beside the
     profile chip, so it should read as a small badge attached to it, not as
     another destination. */
  padding: 0.35rem 0.55rem;
  flex: 0 0 auto;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sun) 16%, var(--paper));
  border: 1.5px solid color-mix(in srgb, var(--sun) 45%, var(--line));
  color: var(--ink-sun);
  font-weight: 800;
  font-size: 0.92rem;
  text-decoration: none;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.credits-chip:hover { color: var(--ink-sun); filter: brightness(1.03); }
.credits-chip.is-low {
  background: color-mix(in srgb, var(--coral) 16%, var(--paper));
  border-color: color-mix(in srgb, var(--coral) 45%, var(--line));
  color: var(--ink-coral);
}

/* Pre-release it is a label, not a control: no pointer, no hover lift, and
   nothing that suggests pressing it will take you somewhere. */
.user-chip--static { cursor: default; }
.user-chip--static:hover { border-color: var(--line); transform: none; }

.user-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.8rem 0.35rem 0.35rem;
  border-radius: var(--r-pill);
  background: var(--paper);
  border: 1.5px solid var(--line);
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  max-inline-size: 190px;
}
.user-chip:hover { color: var(--ink); border-color: var(--brand); }
.avatar {
  inline-size: 30px;
  block-size: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, var(--brand), var(--coral));
  color: #fff;
  font-weight: 900;
  text-transform: uppercase;
  flex: none;
}
.user-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.inline-form { display: inline; }
.linklike {
  background: none;
  border: 0;
  padding: 0.5rem 0.7rem;
  color: var(--ink-soft);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  border-radius: var(--r-pill);
}
.linklike:hover { background: var(--brand-soft); color: var(--brand-deep); }

/* Mobile: let the nav wrap rather than squeezing the ring and chips. */
@media (max-width: 900px) {
  .nav { flex-wrap: wrap; padding-block: 0.6rem; gap: 0.5rem; }
}

/* =========================================================================
   Language picker
   ========================================================================= */
.langpick { position: relative; flex: none; }
.langpick > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.8rem;
  border-radius: var(--r-pill);
  border: 1.5px solid var(--line);
  background: var(--paper);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.langpick > summary::-webkit-details-marker { display: none; }
.langpick > summary:hover { border-color: var(--brand); }
.langpick-name { max-inline-size: 8ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.langpick-caret {
  inline-size: 16px;
  block-size: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform var(--t-fast);
}
.langpick[open] .langpick-caret { transform: rotate(180deg); }

.langpick-menu {
  position: absolute;
  z-index: 60;
  inset-block-start: calc(100% + 0.45rem);
  inset-inline-end: 0;
  min-inline-size: 12rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.35rem;
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  animation: langpick-in 0.18s var(--ease-out) both;
  /* 16 languages overflow the viewport — cap the height and scroll inside the
     menu so the page body never has to scroll to reach the last option. */
  max-block-size: min(70vh, 30rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}
@keyframes langpick-in { from { opacity: 0; transform: translateY(-6px) scale(0.98); } }

.langpick-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  inline-size: 100%;
  padding: 0.5rem 0.7rem;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  font-weight: 700;
  color: var(--ink);
  text-align: start;
  cursor: pointer;
}
.langpick-item:hover { background: var(--brand-soft); }
.langpick-item.is-active { background: var(--brand-soft); color: var(--brand-deep); }
.langpick-label { flex: 1 1 auto; }
.langpick-check { color: var(--grass-deep); font-weight: 900; }

/* =========================================================================
   Layout
   ========================================================================= */
main { padding-block: 2rem 4rem; min-block-size: 60vh; }

.page-head { margin-block-end: 1.6rem; }
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 700;
  text-decoration: none;
  color: var(--ink-soft);
  margin-block-end: 0.6rem;
}
.back-link:hover { color: var(--brand-deep); }

.flash {
  padding: 0.9rem 1.2rem;
  border-radius: var(--r-md);
  margin-block-end: 1rem;
  font-weight: 700;
  border: 1.5px solid;
}
/* Tokenised alongside .flash-info below: these three appear on every form
   action, and hard-coded pale cards stayed lit on a dark page. */
.flash-success { background: color-mix(in srgb, var(--grass) 12%, var(--paper)); border-color: color-mix(in srgb, var(--grass) 45%, var(--line)); color: var(--ink-grass); }
.flash-error   { background: color-mix(in srgb, var(--coral) 12%, var(--paper)); border-color: color-mix(in srgb, var(--coral) 45%, var(--line)); color: var(--ink-coral); }
.flash-info    { background: var(--brand-soft); border-color: color-mix(in srgb, var(--grass) 45%, var(--line)); color: var(--ink-grass); }

/* =========================================================================
   Hero
   ========================================================================= */
.hero {
  position: relative;
  overflow: hidden;
  padding: clamp(1.6rem, 1rem + 3vw, 3rem);
  border-radius: var(--r-xl);
  /* Two neighbouring hues only. Three widely-spaced stops (green→violet→red)
     read as a muddy rainbow; sky→violet stays clean behind white text and
     leaves green free to mean "go" everywhere else. */
  background: linear-gradient(120deg, var(--sky) 0%, var(--violet) 100%);
  color: #fff;
  box-shadow: var(--shadow-lg);
  margin-block-end: 2.2rem;
}
.hero::before,
.hero::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.13);
  pointer-events: none;
}
.hero::before { inline-size: 320px; block-size: 320px; inset-block-start: -120px; inset-inline-end: -80px; }
.hero::after  { inline-size: 200px; block-size: 200px; inset-block-end: -90px; inset-inline-end: 130px; }
.hero h1 { color: #fff; margin-block-end: 0.4rem; }
.hero p { color: rgba(255, 255, 255, 0.94); max-inline-size: 52ch; font-size: 1.06rem; }
.hero-body { position: relative; z-index: 1; }
.hero-mascot {
  position: absolute;
  inset-block-end: -10px;
  inset-inline-end: 5%;
  font-size: clamp(4rem, 9vw, 7rem);
  filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.22));
  animation: bob 3.4s ease-in-out infinite;
  pointer-events: none;
}
@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-14px) rotate(3deg); }
}
@media (max-width: 700px) { .hero-mascot { display: none; } }

/* =========================================================================
   Island map — the roadmap. Modules are islands floating on the canvas.
   ========================================================================= */
.islands {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: 1.6rem;
}

.island {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  padding: 1.5rem 1.3rem 1.3rem;
  border-radius: var(--r-xl);
  background: var(--paper);
  border: 2px solid var(--line);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--ink);
  text-align: center;
  transition: transform var(--t-mid) var(--ease-bounce),
              box-shadow var(--t-mid), border-color var(--t-mid);
}
.island:hover { color: var(--ink); transform: translateY(-7px); box-shadow: var(--shadow-lg); }

.island-emblem {
  inline-size: 96px;
  block-size: 96px;
  margin-inline: auto;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 2.6rem;
  background: linear-gradient(150deg, var(--brand-soft), #dff3ff);
  border: 3px solid var(--paper);
  box-shadow: var(--shadow-md);
  transition: transform var(--t-mid) var(--ease-bounce);
}
.island:hover .island-emblem { transform: scale(1.08) rotate(-5deg); }

.island-index {
  position: absolute;
  inset-block-start: 0.9rem;
  inset-inline-start: 1rem;
  min-inline-size: 30px;
  padding: 0.1rem 0.5rem;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  color: var(--ink-soft);
  font-weight: 900;
  font-size: 0.85rem;
}
.island-name { font-size: 1.12rem; font-weight: 800; margin: 0; }
.island-meta { color: var(--ink-soft); font-size: 0.9rem; }

.island-bar {
  block-size: 10px;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  overflow: hidden;
}
.island-bar > i {
  display: block;
  block-size: 100%;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--grass), var(--sun));
  transition: inline-size 0.7s var(--ease-out);
}

/* --- island states ------------------------------------------------------ */
.island.is-current {
  border-color: var(--brand);
  box-shadow: 0 0 0 5px rgba(108, 76, 241, 0.16), var(--shadow-lg);
}
.island.is-complete { border-color: var(--grass); }
.island.is-complete .island-emblem {
  background: linear-gradient(150deg, #d8f8ea, #b6f0d7);
}
.island.is-locked {
  filter: grayscale(0.85);
  opacity: 0.62;
  cursor: not-allowed;
  pointer-events: none;
}
.island.is-locked .island-emblem { background: var(--canvas-deep); }

.island-flag {
  position: absolute;
  inset-block-start: 0.8rem;
  inset-inline-end: 0.9rem;
  font-size: 1.3rem;
}

.island-cta {
  margin-block-start: auto;
  padding-block-start: 0.3rem;
  font-weight: 800;
  color: var(--brand-deep);
}
.island.is-complete .island-cta { color: var(--grass-deep); }

/* "You are here" pulse on the current island. */
.island.is-current .island-emblem::after {
  content: "";
  position: absolute;
  inline-size: 96px;
  block-size: 96px;
  border-radius: 50%;
  border: 3px solid var(--brand);
  animation: ping 2s var(--ease-out) infinite;
}
@keyframes ping {
  0%   { transform: scale(1);    opacity: 0.75; }
  100% { transform: scale(1.45); opacity: 0; }
}
/* =========================================================================
   Island detail — lessons as stepping stones on an inward spiral.
   Positions (--x/--y) come from the view so the trail needs no JS. --x feeds
   inset-inline-start, which mirrors itself in RTL for free.
   ========================================================================= */
.spiral {
  position: relative;
  inline-size: min(var(--box), 100%);
  aspect-ratio: 1;
  margin: 1rem auto 0;
  color: var(--line);
}

.spiral-path {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}
/* The stones are placed with inset-inline-start, which mirrors itself in RTL.
   The trail is an SVG path whose coordinates are absolute and do NOT mirror,
   so in Persian/Arabic/Urdu the stones flipped and the line stayed put. Mirror
   the whole SVG to match. (Same fix as .voyage-route in islands.css.) */
[dir="rtl"] .spiral-path { transform: scaleX(-1); }

/* Draw the trail on first paint, so the island reveals itself as a path. */
.js .spiral-path path {
  stroke-dasharray: 1.6 2.4;
  animation: trail-draw 1.1s var(--ease-out) both;
}
@keyframes trail-draw { from { opacity: 0; } to { opacity: 1; } }

.stone {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  translate: -50% -50%;
  inline-size: 76px;
  display: grid;
  place-items: center;
  text-decoration: none;
  color: var(--ink);
  animation: stone-pop 0.5s var(--ease-bounce) both;
  animation-delay: var(--d, 0s);
}
/* RTL mirrors the inline axis, so the translate has to mirror with it or the
   stone lands a half-width off its point. */
[dir="rtl"] .stone { translate: 50% -50%; }

@keyframes stone-pop {
  from { opacity: 0; scale: 0.4; }
  to   { opacity: 1; scale: 1; }
}

.stone-dot {
  inline-size: 62px;
  block-size: 62px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--paper);
  border: 3px solid var(--line);
  box-shadow: 0 5px 0 var(--line), var(--shadow-sm);
  font-weight: 900;
  font-size: 1.2rem;
  transition: transform var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}
.stone:hover .stone-dot { transform: translateY(-5px) scale(1.07); }
.stone:active .stone-dot { transform: translateY(2px); box-shadow: 0 1px 0 var(--line); }

/* The label sits under the stone and is clipped to two lines so a long title
   can never push into a neighbouring stone. */
.stone-label {
  position: absolute;
  inset-block-start: 68px;
  /* Keep in step with SPIRAL_LABEL_PX in courses/views.py: the spiral canvas
     is sized from this number, and a test fails if the two drift apart. */
  inline-size: 112px;
  text-align: center;
  pointer-events: none;
}
.stone-label b {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 0.83rem;
  line-height: 1.35;
}
.stone-label small { display: block; font-size: 0.72rem; color: var(--ink-faint); }

.stone.is-complete .stone-dot {
  background: var(--grass);
  border-color: var(--grass-deep);
  box-shadow: 0 5px 0 var(--grass-deep), var(--shadow-sm);
  color: #fff;
}
.stone.is-current .stone-dot {
  background: var(--brand);
  border-color: var(--brand-deep);
  box-shadow: 0 5px 0 var(--brand-deep), 0 0 0 8px rgba(108, 76, 241, 0.16);
  color: #fff;
  animation: stone-beacon 2.2s ease-in-out infinite;
}
@keyframes stone-beacon {
  0%, 100% { box-shadow: 0 5px 0 var(--brand-deep), 0 0 0 6px rgba(108, 76, 241, 0.18); }
  50%      { box-shadow: 0 5px 0 var(--brand-deep), 0 0 0 16px rgba(108, 76, 241, 0); }
}
.stone.is-current .stone-label b { color: var(--brand-deep); }

/* A lesson the teacher has not opened yet. Greyed and not a link, the same
   language the roadmap already speaks for an island you have not reached — it
   is a "not yet", not a failure, so it stays legible rather than hidden. */
.stone.is-shut { cursor: default; opacity: 0.55; filter: grayscale(1); }
.stone.is-shut .stone-dot {
  background: var(--paper);
  border-color: var(--line);
  box-shadow: 0 5px 0 var(--line), var(--shadow-sm);
  color: var(--ink-faint);
}
.stone.is-shut .stone-label b { color: var(--ink-faint); }

/* A spiral needs width. On a phone there is none, so fall back to a simple
   ordered list — same DOM, no repositioning, nothing overlaps. */
@media (max-width: 760px) {
  .spiral {
    aspect-ratio: auto;
    inline-size: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }
  .spiral-path { display: none; }
  .stone {
    position: static;
    translate: none;
    inline-size: 100%;
    grid-template-columns: auto 1fr;
    justify-items: start;
    gap: 0.9rem;
    padding: 0.6rem 0.9rem;
    background: var(--paper);
    border: 2px solid var(--line);
    border-radius: var(--r-md);
  }
  [dir="rtl"] .stone { translate: none; }
  .stone-dot { inline-size: 46px; block-size: 46px; font-size: 1rem; }
  .stone-label {
    position: static;
    inline-size: auto;
    text-align: start;
  }
  .stone-label b { -webkit-line-clamp: 1; font-size: 0.95rem; }
}


.trail-goal {
  display: grid;
  place-items: center;
  gap: 0.3rem;
  padding: 1.2rem;
  text-align: center;
}


@media (max-width: 640px) {
  .trail::before { inset-inline-start: 34px; translate: 0 0; }
  .stone-row, .stone-row.side-left, .stone-row.side-right { justify-content: stretch; }
  .stone { inline-size: 100%; }
}

/* =========================================================================
   Lesson page
   ========================================================================= */
.lesson-layout { display: grid; gap: 1.6rem; }
/* Full width of the sheet, exactly like the cards on a challenge page.
   It used to be capped at --measure inside a grid, and a max-width grid item
   with the default justify-items sits at the START of its track rather than
   in the middle — so the lesson text hugged one edge while its own heading
   spanned the full width above it, and the page looked lopsided. The reading
   measure now lives on the prose itself, centred, so long paragraphs stay
   comfortable without dragging the whole column sideways. */
.lesson-body { min-inline-size: 0; }

.video-frame {
  position: relative;
  padding-block-end: 56.25%;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  /* Room ABOVE as well as below: the video sits under the lesson text now, and
     with a margin on one side only it grew straight out of the last sentence. */
  margin-block: 1.5rem;
  background: #000;
}
.video-frame iframe {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  border: 0;
}

/* --- "I read this" checkbox -------------------------------------------- */
.read-check {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 1.05rem 1.3rem;
  border-radius: var(--r-lg);
  background: var(--paper);
  border: 2px dashed var(--line);
  cursor: pointer;
  user-select: none;
  transition: border-color var(--t-fast), background var(--t-fast);
  font-weight: 700;
  margin-block: 1.6rem;
}
.read-check:hover { border-color: var(--grass); }
.read-check input { position: absolute; opacity: 0; pointer-events: none; }

.read-box {
  inline-size: 34px;
  block-size: 34px;
  flex: none;
  border-radius: 10px;
  border: 3px solid var(--line);
  background: var(--paper);
  display: grid;
  place-items: center;
  color: transparent;
  font-weight: 900;
  font-size: 1.1rem;
  transition: background var(--t-fast) var(--ease-bounce),
              border-color var(--t-fast), color var(--t-fast), transform var(--t-fast);
}
.read-check.is-read {
  border-style: solid;
  border-color: var(--grass);
  background: color-mix(in srgb, var(--grass) 8%, var(--paper));
}
.read-check.is-read .read-box {
  background: var(--grass);
  border-color: var(--grass);
  color: #fff;
  transform: scale(1.06);
}
.read-check-label { flex: 1 1 auto; }
.read-check-hint { display: block; font-weight: 600; font-size: 0.85rem; color: var(--ink-soft); }

/* --- prev / next -------------------------------------------------------- */
.lesson-nav {
  display: flex;
  gap: 0.8rem;
  justify-content: space-between;
  align-items: stretch;
  margin-block-start: 2.2rem;
  padding-block-start: 1.4rem;
  border-block-start: 2px solid var(--line);
}
.lesson-nav-btn {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: 1 1 0;
  max-inline-size: 46%;
  padding: 0.85rem 1.1rem;
  border-radius: var(--r-md);
  background: var(--paper);
  border: 2px solid var(--line);
  text-decoration: none;
  color: var(--ink);
  font-weight: 800;
  transition: border-color var(--t-fast), transform var(--t-fast);
}
.lesson-nav-btn:hover { color: var(--ink); border-color: var(--brand); transform: translateY(-3px); }
.lesson-nav-btn.is-next { text-align: end; margin-inline-start: auto; }
.lesson-nav-dir {
  font-size: 0.78rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}
.lesson-nav-title {
  font-size: 0.98rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The arrow glyphs must flip in RTL — they point along reading direction. */
[dir="rtl"] .lesson-nav-arrow { display: inline-block; scale: -1 1; }

/* =========================================================================
   Quiz
   ========================================================================= */
.quiz-card { margin-block: 1.6rem; }
/* The question is markdown now, so it owns block spacing rather than being a
   single bold line. Its own paragraphs stay bold; a code block inside it is
   left alone to be code. */
.quiz-q { margin-block-end: 0.7rem; }
.quiz-q > p { font-weight: 800; margin-block: 0 0.5rem; }
.quiz-q > p:last-child { margin-block-end: 0; }
.quiz-q pre { margin-block: 0.6rem; }
.quiz-choices { display: grid; gap: 0.55rem; }
.quiz-choice {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  inline-size: 100%;
  padding: 0.8rem 1rem;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--paper);
  font: inherit;
  font-weight: 700;
  text-align: start;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast), transform var(--t-fast);
}
.quiz-choice:hover { border-color: var(--brand); transform: translateX(2px); }
.quiz-choice.is-correct { border-color: var(--grass); background: color-mix(in srgb, var(--grass) 10%, var(--paper)); color: var(--ink); }
.quiz-choice.is-wrong   { border-color: var(--coral); background: color-mix(in srgb, var(--coral) 10%, var(--paper)); color: var(--ink); }
.quiz-choice:disabled { cursor: default; transform: none; }
.quiz-verdict { margin-block-start: 0.7rem; font-weight: 800; }

/* =========================================================================
   Challenge pills
   ========================================================================= */
.challenges { display: grid; gap: 0.7rem; }
.challenge {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.85rem 1.1rem;
  border-radius: var(--r-md);
  background: var(--paper);
  border: 2px solid var(--line);
  text-decoration: none;
  color: var(--ink);
  font-weight: 700;
  transition: border-color var(--t-fast), transform var(--t-fast);
}
.challenge:hover { color: var(--ink); border-color: var(--brand); transform: translateY(-3px); }
.challenge.is-done { border-color: var(--grass); }
.challenge-stars { margin-inline-start: auto; color: var(--sun-deep); font-size: 1.05rem; flex: none; }
.challenge-icon { font-size: 1.3rem; flex: none; }

/* =========================================================================
   Robot chat widget
   ========================================================================= */
.robot-panel {
  position: fixed;
  inset-block-end: 6.4rem;
  inset-inline-end: 1.4rem;
  z-index: 96;
  inline-size: min(390px, calc(100vw - 2rem));
  max-block-size: min(560px, calc(100vh - 9rem));
  display: flex;
  flex-direction: column;
  border-radius: var(--r-lg);
  background: var(--paper);
  border: 2px solid var(--line);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: panel-in 0.22s var(--ease-out) both;
}
@keyframes panel-in {
  from { opacity: 0; transform: translateY(18px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}
@keyframes panel-fade { from { opacity: 0; } to { opacity: 1; } }
.robot-panel[hidden] { display: none; }

.robot-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1rem;
  background: linear-gradient(120deg, var(--brand), var(--sky));
  color: #fff;
}
.robot-head h3 { margin: 0; font-size: 1rem; color: #fff; flex: 1 1 auto; }
.robot-head-credits {
  font-size: 0.82rem;
  font-weight: 800;
  background: rgba(255, 255, 255, 0.22);
  padding: 0.15rem 0.6rem;
  border-radius: var(--r-pill);
  font-variant-numeric: tabular-nums;
}
.robot-close {
  background: none;
  border: 0;
  color: #fff;
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.2rem;
}

.robot-log {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  background: var(--canvas);
}
.robot-msg {
  max-inline-size: 86%;
  padding: 0.7rem 0.95rem;
  border-radius: var(--r-md);
  font-size: 0.95rem;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  animation: msg-in 0.2s var(--ease-out) both;
}
@keyframes msg-in { from { opacity: 0; transform: translateY(6px); } }
.robot-msg.from-bot {
  align-self: flex-start;
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-start-start-radius: 4px;
}
.robot-msg.from-me {
  align-self: flex-end;
  background: var(--brand);
  color: #fff;
  border-start-end-radius: 4px;
}
.robot-msg.is-error {
  align-self: stretch;
  max-inline-size: none;
  background: #ffeef1;
  border: 1.5px solid #f6b3bf;
  color: #96233a;
  font-weight: 700;
}
/* The way out when Pity is done for the day. It sits inside an error bubble,
   so it must not inherit that bubble's muted ink. */
.robot-help-link {
  display: inline-block;
  margin-block-start: 0.35rem;
  font-weight: 800;
  color: var(--link);
}
.robot-cost {
  display: block;
  margin-block-start: 0.35rem;
  font-size: 0.76rem;
  font-weight: 800;
  color: var(--ink-faint);
}

.robot-typing { display: inline-flex; gap: 4px; align-items: center; }
.robot-typing i {
  inline-size: 7px;
  block-size: 7px;
  border-radius: 50%;
  background: var(--ink-faint);
  animation: blink 1.2s infinite;
}
.robot-typing i:nth-child(2) { animation-delay: 0.2s; }
.robot-typing i:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0.25; } 30% { opacity: 1; } }

.robot-foot {
  display: flex;
  gap: 0.5rem;
  padding: 0.7rem;
  border-block-start: 1.5px solid var(--line);
  background: var(--paper);
}
.robot-input {
  flex: 1 1 auto;
  min-inline-size: 0;
  padding: 0.65rem 0.9rem;
  border-radius: var(--r-pill);
  border: 2px solid var(--line);
  font: inherit;
  font-size: 0.95rem;
  resize: none;
  max-block-size: 96px;
}
.robot-input:focus { border-color: var(--brand); outline: none; }
.robot-send {
  flex: none;
  inline-size: 44px;
  block-size: 44px;
  border-radius: 50%;
  border: 0;
  background: var(--brand);
  color: #fff;
  font-size: 1.1rem;
  cursor: pointer;
}
.robot-send:disabled { opacity: 0.45; cursor: not-allowed; }
/* The paper-plane points along reading direction. */
[dir="rtl"] .robot-send span { display: inline-block; scale: -1 1; }

/* Quick asks: the way in for a child who opens Pity and does not know what to
   type. They sit above the box, wrap on a phone, and go away once used. */
.robot-asks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  /* Breathing room under the chips: they sat directly on the input's top
     border, which read as one control rather than a suggestion above a box. */
  padding: 0.5rem 0.9rem 0.7rem;
}
.robot-ask {
  padding: 0.3rem 0.6rem;
  border: 1.5px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--ink-soft);
  font: 600 0.78rem/1.3 var(--ui-font);
  cursor: pointer;
  text-align: start;
}
.robot-ask:hover { border-color: var(--brand); color: var(--ink); }

/* Retry lives inside an error bubble, so it must not inherit that muted ink. */
.robot-retry {
  margin-block-start: 0.4rem;
  padding: 0.25rem 0.7rem;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--brand);
  color: #fff;
  font: 800 0.78rem/1.3 var(--ui-font);
  cursor: pointer;
}
.robot-retry:hover { background: var(--brand-deep); }
.robot-retry:disabled { opacity: 0.5; cursor: default; }

.robot-note {
  padding: 0.5rem 1rem 0.7rem;
  font-size: 0.78rem;
  color: var(--ink-faint);
  text-align: center;
  background: var(--paper);
}

@media (max-width: 520px) {
  .robot-panel {
    inset-inline: 0.6rem;
    inset-block-end: 5.8rem;
    inline-size: auto;
  }
}

/* =========================================================================
   Certificate
   ========================================================================= */
.cert-sheet {
  position: relative;
  max-inline-size: 860px;
  margin-inline: auto;
  padding: clamp(1.6rem, 1rem + 3vw, 3.2rem);
  border-radius: var(--r-lg);
  background:
    radial-gradient(700px 340px at 50% 0%, color-mix(in srgb, var(--sun) 10%, var(--paper)) 0%, transparent 70%),
    var(--paper);
  border: 3px solid var(--sun);
  box-shadow: var(--shadow-lg);
  text-align: center;
  overflow: hidden;
}
/* Inner hairline frame — the classic certificate double-border. */
.cert-sheet::before {
  content: "";
  position: absolute;
  inset: 12px;
  border: 2px solid var(--canvas-deep);
  border-radius: var(--r-md);
  pointer-events: none;
}
.cert-inner { position: relative; z-index: 1; }

.cert-ribbon {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 1.2rem;
  border-radius: var(--r-pill);
  font-weight: 900;
  font-size: 0.92rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-block-end: 1rem;
}
.cert-ribbon.grade-gold   { background: linear-gradient(135deg, #fff0c2, #ffd970); color: #7a5200; }
.cert-ribbon.grade-silver { background: linear-gradient(135deg, #f1f3f7, #d5dae4); color: #4c5566; }
.cert-ribbon.grade-bronze { background: linear-gradient(135deg, #fbe0c8, #eab183); color: #7a4415; }

.cert-title {
  font-size: clamp(1.6rem, 1.1rem + 2.4vw, 2.5rem);
  margin-block-end: 0.2rem;
}
.cert-lead { color: var(--ink-soft); }
.cert-name {
  font-size: clamp(1.7rem, 1.2rem + 2.6vw, 2.7rem);
  font-weight: 900;
  color: var(--brand-deep);
  margin-block: 0.6rem;
  padding-block-end: 0.6rem;
  border-block-end: 3px solid var(--canvas-deep);
  display: inline-block;
  padding-inline: 1.5rem;
}
.cert-course { font-weight: 800; font-size: 1.15rem; margin-block: 0.8rem; }

/* Just the number, centred.
   It used to sit inside a conic-gradient ring, which reads as a progress bar
   on a document that is only ever issued at 100% completion: the ring could
   never be partial, so it measured nothing and drew the eye to a shape that
   was always full. The score is the fact; the ring was decoration pretending
   to be data. */
.cert-score {
  display: block;
  text-align: center;
  margin-block: 1.4rem;
}
.cert-score-num {
  display: block;
  font-size: 3.2rem;
  font-weight: 900;
  color: var(--ink);
  line-height: 1;
}
.cert-score-cap {
  display: block;
  margin-block-start: 0.35rem;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.cert-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1rem;
  margin-block-start: 1.6rem;
  padding-block-start: 1.4rem;
  border-block-start: 2px solid var(--canvas-deep);
}
.cert-field-label {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 800;
  color: var(--ink-faint);
}
.cert-field-value { font-weight: 800; }

/* Three fields across two columns leaves the third one alone on its own row,
   sitting in the first cell with the rest of the row empty beside it — which
   on a phone reads as "credits left" shoved against one margin. When it is
   the odd one out it takes the whole row instead, so it lands in the middle
   under the pair. :nth-child(odd) is what keeps this from firing on the
   three-across desktop layout, where nothing is orphaned. */
@media (max-width: 600px) {
  .cert-grid { grid-template-columns: repeat(2, 1fr); }
  .cert-grid > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
.cert-serial { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.05em; }

.cert-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  justify-content: center;
  margin-block-start: 1.6rem;
}

.cert-example-note {
  max-inline-size: 860px;
  margin: 0 auto 1.2rem;
  padding: 0.9rem 1.2rem;
  border-radius: var(--r-md);
  background: var(--brand-soft);
  border: 1.5px solid #cdbcff;
  color: var(--ink-grass);
  font-weight: 700;
  text-align: center;
}

@media print {
  /* Everything that belongs to the website rather than to the document. A
     certificate that prints "Back to the map" is not a certificate. */
  .site-header, .site-footer, .cert-actions, .robot-fab, .robot-panel,
  .cert-example-note, .skip-link, .back-link, .nav, .langpick,
  .messages, .flash, .scratch, .ex-next { display: none !important; }
  body { background: #fff; }
  .cert-sheet { box-shadow: none; border-color: #d9b45a; }
}

/* --- locked certificate ------------------------------------------------- */
.cert-locked {
  max-inline-size: 620px;
  margin-inline: auto;
  text-align: center;
  padding: 2.4rem 1.6rem;
}
.cert-locked-emoji { font-size: 4rem; display: block; margin-block-end: 0.6rem; filter: grayscale(0.4); }
.cert-progress-ring {
  inline-size: 150px;
  block-size: 150px;
  margin: 1.2rem auto;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: conic-gradient(var(--grass) calc(var(--p, 0) * 1%), var(--canvas-deep) 0);
  position: relative;
}
.cert-progress-ring::before {
  content: "";
  position: absolute;
  inset: 13px;
  border-radius: 50%;
  background: var(--paper);
}
.cert-progress-num { position: relative; font-size: 2rem; font-weight: 900; }

/* =========================================================================
   Forms
   ========================================================================= */
.form-card { max-inline-size: 460px; margin-inline: auto; }
.field { margin-block-end: 1rem; }
.field label { display: block; font-weight: 800; margin-block-end: 0.3rem; }
.field input,
.field select,
.field textarea {
  inline-size: 100%;
  padding: 0.7rem 0.9rem;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--paper);
  font: inherit;
  color: var(--ink);
}
.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--brand); outline: none; }
.helptext { font-size: 0.85rem; color: var(--ink-soft); }
.errorlist { color: var(--ink-coral); font-weight: 700; padding-inline-start: 1.1rem; margin-block: 0.3rem; }

.table { inline-size: 100%; border-collapse: collapse; }
.table th, .table td {
  padding: 0.7rem 0.8rem;
  text-align: start;
  border-block-end: 1.5px solid var(--line);
}
.table th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-faint); }
.table-wrap { overflow-x: auto; }

/* =========================================================================
   Footer
   ========================================================================= */
.site-footer {
  border-block-start: 1.5px solid var(--line);
  background: color-mix(in srgb, var(--paper) 60%, transparent);
  padding-block: 1.4rem;
  color: var(--ink-soft);
  font-size: 0.92rem;
}
.site-footer .container { text-align: center; }
/* Dimmed so the separator divides without competing with either side. */
.site-footer .footer-sep { opacity: 0.45; margin-inline: 0.5rem; }
/* Underlined on purpose: this is the only way to reach a person, and a child
   locked out of their account has to be able to see that it is a link. */
.site-footer a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.site-footer a:hover, .site-footer a:focus-visible { color: var(--link); }

/* =========================================================================
   Empty states
   ========================================================================= */
.empty {
  text-align: center;
  padding: 3rem 1.5rem;
  color: var(--ink-soft);
}
.empty-emoji { font-size: 3.4rem; display: block; margin-block-end: 0.5rem; }

/* =========================================================================
   Motion preferences — respected globally, last so it wins.
   ========================================================================= */
/* =========================================================================
   Scientific team — the public page of the people behind the course, and the
   same circle at list size in the staff panel.

   Two things make this unlike every other card grid here. A card can carry two
   directions at once (an English name above a Persian job title, because
   nobody supplied an English one), and a photograph can be absent. Both are
   handled without a line of JavaScript.
   ========================================================================= */
.team-grid {
  display: grid;
  /* 260px is the width at which the longest real job title,
     "دانشجوی دکتری علوم‌کامپیوتر", sets on two lines under the circle. No
     column breakpoint: auto-fill gives three across on a laptop, two on a
     tablet and one on a phone, with nothing to keep in step by hand. */
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1.4rem;
  list-style: none;
  margin: 0;
  padding-inline-start: 0;
}

.team-card {
  --photo: clamp(96px, 26vw, 132px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  padding: 1.6rem 1.1rem 1.2rem;
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  /* Centred, and not by taste. text-align: start resolves per element against
     THAT element's own direction, so an ltr name and an rtl job title would
     hang off opposite edges of the same card. Centre is the only alignment
     that is right in both. */
  text-align: center;
}

/* --- the circle --------------------------------------------------------- */
.team-photo,
.team-thumb {
  position: relative;
  aspect-ratio: 1;
  flex: none;
  border-radius: 50%;
  overflow: hidden;
  background: var(--brand-soft);
}
.team-photo { inline-size: var(--photo, 132px); }
.team-thumb { inline-size: 52px; }

/* The stand-in when there is no photo yet, sitting UNDER the image slot so a
   file that has gone missing leaves the circle standing rather than a hole. */
.team-initial {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 2.4rem;
  font-weight: 900;
  line-height: 1;
  color: var(--brand-deep);
}
.team-thumb .team-initial { font-size: 1.2rem; }

/* .team-card in the selector on purpose: `.page img` earlier in this file
   already claims border-radius and auto margins for every image in a sheet, at
   the same specificity a bare `.team-photo img` would have, and a tie is
   settled by source order. These win outright. */
.team-card .team-photo img,
.team-photo-preview img,
.team-thumb img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  border-radius: 50%;
  /* No photograph off a phone is square. cover fills the circle instead of
     squashing the face, and the upward bias matters because what a tall
     picture loses to a square crop is its bottom, not its top. On a square
     file this line does nothing. */
  object-fit: cover;
  object-position: 50% 30%;
}
.team-photo-preview { inline-size: 132px; margin-block: 0.4rem; }

/* --- the words ---------------------------------------------------------- */
.team-name {
  margin: 0;
  font-size: 1.12rem;
  /* 800 maps onto Estedad Bold, so a Persian name gets a real bold rather than
     a synthetic one. */
  font-weight: 800;
  line-height: 1.35;
  color: var(--ink);
}
.team-position {
  margin: 0;
  font-size: 0.94rem;
  line-height: 1.7;          /* Persian sits lower and needs the leading */
  color: var(--ink-soft);
  max-inline-size: 24ch;
}

/* --- LinkedIn ----------------------------------------------------------- */
.team-in {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px, the smallest thing a thumb hits reliably. The mark inside is 24. */
  inline-size: 44px;
  block-size: 44px;
  /* The grid stretches rows to the tallest card, so this puts every icon on a
     row onto one line whatever the job titles above them do. */
  margin-block-start: auto;
  border-radius: var(--r-md);
  color: var(--linkedin);
  text-decoration: none;
  transition: color var(--t-fast), transform var(--t-fast) var(--ease-out);
}
.team-in svg { inline-size: 24px; block-size: 24px; display: block; }
.team-in:hover { color: var(--linkedin-deep); transform: translateY(-2px); }

@media (max-width: 560px) {
  /* One column already by then; just tighter, so a whole team is fewer
     screens. */
  .team-card { padding: 1.3rem 0.9rem 1rem; gap: 0.45rem; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Markdown inside a chat bubble: the shared markdown.css is tuned for a full
   lesson page, so tighten it for a 320px-wide bubble. */
.robot-msg.markdown-body > *:first-child { margin-block-start: 0; }
.robot-msg.markdown-body > *:last-child  { margin-block-end: 0; }
.robot-msg.markdown-body p { margin-block-end: 0.6em; }
/* ...but not a paragraph that IS a list item's text — its bottom margin would
   re-open the gap the shared rule above just closed, stranding the number. */
.robot-msg.markdown-body li > p:last-child { margin-block-end: 0; }
.robot-msg.markdown-body pre {
  margin-block: 0.6em;
  padding: 0.7rem 0.85rem;
  font-size: 0.84rem;
  border-radius: var(--r-sm);
  /* A code block is `white-space: pre` and must stay that way — Python is
     whitespace-significant, so wrapping it would be a lie about the code. It
     therefore has to scroll inside its OWN box: without this, one long line
     made the bubble 631px wide inside a 375px phone and the answer ran off
     the side of the screen. The bubble is the scroll container, never the
     page. */
  max-inline-size: 100%;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
}
/* The <code> inside is what actually carries the long line. */
.robot-msg.markdown-body pre code { display: block; min-inline-size: 0; }
.robot-msg.markdown-body :not(pre) > code { font-size: 0.86em; }
.robot-msg.markdown-body ul,
.robot-msg.markdown-body ol { padding-inline-start: 1.2rem; margin-block: 0.5em; }

/* =========================================================================
   Code editor — dark IDE surface.

   The editor is a transparent <textarea> sitting exactly on top of a <pre>
   that paints the syntax colours (editor.js builds both). For the caret to
   land on the character the student sees, EVERY metric that affects text
   layout must match between the two: font, size, line-height, letter-spacing,
   padding, border width, tab-size and white-space. Change one, change both.
   ========================================================================= */
:root {
  --ide-bg:      #12121f;
  --ide-bg-soft: #1b1b2e;
  --ide-line:    #2c2c46;
  --ide-text:    #e7e5f5;
  --ide-kw:      #ff8ab8;   /* keywords: def, for, if      */
  --ide-blt:     #7bd3ff;   /* builtins: print, range      */
  --ide-str:     #9ae88a;   /* strings                      */
  --ide-num:     #ffc978;   /* numbers                      */
  --ide-com:     #7a7899;   /* comments                     */

  --ide-font: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
              "Liberation Mono", monospace;
  --ide-size: 0.95rem;
  --ide-lh: 1.65;
  --ide-pad: 0.95rem;
  /* Room for the line-number gutter. Three digits sit comfortably; a
     four-digit lesson would be a lesson nobody should have written. */
  --ide-gutter: 2.6rem;
}

.editor-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  padding: 0.4rem 0.55rem;
  background: var(--ide-bg-soft);
  border: 1.5px solid var(--ide-line);
  border-block-end: 0;
  border-start-start-radius: var(--r-md);
  border-start-end-radius: var(--r-md);
}

/* On a phone the virtual keyboard shoves the page up to keep the caret in
   view, and the toolbar went with it — so ( ) * " , the symbols a phone
   keyboard buries two taps deep, left the screen the moment a child started
   typing and had to be scrolled back to for every one of them. Sticky, just
   under the site header, so it stays where the thumb already is.
   Phones only: on a desktop there is no keyboard to make room for. */
@media (max-width: 760px) {
  .editor-toolbar {
    position: sticky;
    /* The measured header height (app.js); --nav-h is only its minimum. */
    inset-block-start: var(--header-h, var(--nav-h));
    /* Under the header (90) and over the code it now scrolls across. */
    z-index: 5;
  }
}
.tb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 34px;
  block-size: 32px;
  padding: 0 0.5rem;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #b9b6d6;
  font: inherit;
  font-family: var(--ide-font);
  font-size: 0.9rem;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.tb-btn:hover { background: #2a2a44; color: #fff; }
.tb-btn:active { background: #35355a; }
.tb-sep {
  inline-size: 1.5px;
  block-size: 20px;
  margin-inline: 0.3rem;
  background: var(--ide-line);
  border-radius: 2px;
}

.editor-wrap {
  position: relative;
  /* Pinned LTR like the code itself, so the gutter is on the same side and
     line 1 is in the same corner whatever the page language. */
  direction: ltr;
  background: var(--ide-bg);
  border: 1.5px solid var(--ide-line);
  border-end-start-radius: var(--r-md);
  border-end-end-radius: var(--r-md);
  overflow: hidden;
}

/* Shared text metrics. Both layers get these so glyphs line up exactly. */
.code-highlight,
[data-editor-code] {
  margin: 0;
  padding: var(--ide-pad);
  border: 0;
  font-family: var(--ide-font);
  font-size: var(--ide-size);
  line-height: var(--ide-lh);
  letter-spacing: normal;
  tab-size: 4;
  white-space: pre;
  overflow-wrap: normal;
  word-break: normal;
  /* Both layers step aside for the gutter by the same amount, or the caret
     drifts away from the glyph the child can see. */
  padding-inline-start: calc(var(--ide-gutter) + 0.85rem);
}

/* The line numbers. Same font, size and line-height as the two layers it sits
   beside — that is the whole trick: number N has to sit on line N, and it only
   does if all three agree about how tall a line is. It scrolls vertically with
   the textarea and never horizontally, so a long line slides underneath it. */
.code-gutter {
  position: absolute;
  inset-block: 0;
  left: 0;
  inline-size: var(--ide-gutter);
  margin: 0;
  padding-block: var(--ide-pad);
  padding-inline: 0 0.5rem;
  font-family: var(--ide-font);
  font-size: var(--ide-size);
  line-height: var(--ide-lh);
  white-space: pre;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--ide-com);
  background: var(--ide-bg);
  border-inline-end: 1.5px solid var(--ide-line);
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}

.code-highlight {
  position: absolute;
  inset: 0;
  overflow: auto;
  color: var(--ide-text);
  pointer-events: none;
  /* The textarea scrolls; this mirrors it, so its own bars must not show. */
  scrollbar-width: none;
}
.code-highlight::-webkit-scrollbar { display: none; }

[data-editor-code] {
  position: relative;
  display: block;
  inline-size: 100%;
  min-block-size: 340px;
  background: transparent;
  /* Text is invisible; the overlay underneath supplies the colour. The caret
     and the selection stay visible so typing still feels normal. */
  color: transparent;
  caret-color: #fff;
  resize: vertical;
  overflow: auto;
}
[data-editor-code]::selection { background: rgba(124, 92, 255, 0.45); color: transparent; }
[data-editor-code]:focus { outline: none; }
.editor-wrap:focus-within { box-shadow: inset 0 0 0 2px var(--brand); }

.tok-kw  { color: var(--ide-kw);  font-weight: 700; }
.tok-blt { color: var(--ide-blt); }
.tok-str { color: var(--ide-str); }
.tok-num { color: var(--ide-num); }
.tok-com { color: var(--ide-com); font-style: italic; }

/* Autocomplete strip — sits above the editor, scrolls sideways on mobile. */
.suggest-strip {
  display: flex;
  gap: 0.3rem;
  overflow-x: auto;
  padding: 0.35rem 0.5rem;
  background: var(--ide-bg-soft);
  border: 1.5px solid var(--ide-line);
  border-block-end: 0;
  scrollbar-width: thin;
}
.suggest-strip button {
  flex: none;
  padding: 0.25rem 0.7rem;
  border: 1px solid var(--ide-line);
  border-radius: var(--r-pill);
  background: #24243c;
  color: var(--ide-blt);
  font-family: var(--ide-font);
  font-size: 0.84rem;
  cursor: pointer;
}
.suggest-strip button:hover { background: #30304e; color: #fff; }

/* =========================================================================
   Navbar progress ring — a pie that fills as the course is completed.
   The sweep is driven entirely by --p (0-100) so JS updates one variable.
   ========================================================================= */
.nav-ring {
  --p: 0;
  --ring: 42px;
  position: relative;
  display: inline-grid;
  place-items: center;
  inline-size: var(--ring);
  block-size: var(--ring);
  flex: none;
  text-decoration: none;
  color: var(--brand-deep);
}
.nav-ring-pie {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* calc(var(--p) * 1%) is what makes this animatable and JS-friendly. */
  background: conic-gradient(
    var(--brand) calc(var(--p) * 1%),
    var(--canvas-deep) 0
  );
  transition: background var(--t-mid) var(--ease-out);
}
/* Punch the middle out so it reads as a ring, not a pie chart. */
.nav-ring-pie::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--paper);
}
.nav-ring-value {
  position: relative;
  font-size: 0.72rem;
  font-weight: 900;
  line-height: 1;
  /* Tighter than before: this held two digits and a "%", and now holds up to
     four digits inside a 34px disc. Tabular figures because the score is
     repainted in place after a read or a solve, and proportional digits make
     the whole number shuffle sideways when a 1 becomes a 4. */
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.nav-ring:hover .nav-ring-pie { filter: brightness(1.08); }
.nav-ring:hover { color: var(--brand); }

/* At 100% the ring goes gold and gently pulses — the certificate is waiting. */
.nav-ring[style*="--p: 100"] .nav-ring-pie {
  background: conic-gradient(var(--sun) 100%, var(--sun) 0);
  animation: ring-done 2.4s ease-in-out infinite;
}
@keyframes ring-done {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 176, 32, 0.5); }
  50%      { box-shadow: 0 0 0 7px rgba(255, 176, 32, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .nav-ring-pie { transition: none; animation: none; }
}

/* =========================================================================
   Celebration — confetti + a "you did it" card on a perfect solve.
   ========================================================================= */
.confetti-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 300;
  overflow: hidden;
}
.confetti-layer i {
  position: absolute;
  inset-block-start: -24px;
  opacity: 0.95;
  animation-name: confetti-fall;
  animation-timing-function: cubic-bezier(0.35, 0.1, 0.6, 1);
  animation-fill-mode: forwards;
}
@keyframes confetti-fall {
  from { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 1; }
  to   { transform: translate3d(var(--drift, 0), 105vh, 0) rotate(var(--rot, 360deg)); opacity: 0.15; }
}

.celebrate-veil {
  position: fixed;
  inset: 0;
  z-index: 310;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: rgba(18, 16, 40, 0.55);
  backdrop-filter: blur(5px);
  animation: veil-in 0.2s var(--ease-out) both;
}
.celebrate-veil.is-leaving { animation: veil-in 0.2s var(--ease-out) reverse both; }
@keyframes veil-in { from { opacity: 0; } to { opacity: 1; } }

.celebrate-card {
  inline-size: min(400px, 100%);
  padding: 2rem 1.75rem 1.75rem;
  text-align: center;
  background: var(--paper);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  animation: celebrate-pop 0.45s var(--ease-bounce) both;
}
@keyframes celebrate-pop {
  from { opacity: 0; transform: translateY(24px) scale(0.86); }
  to   { opacity: 1; transform: none; }
}
.celebrate-emoji {
  display: block;
  font-size: 3.6rem;
  line-height: 1;
  margin-block-end: 0.5rem;
  animation: celebrate-bob 1.6s ease-in-out infinite;
}
@keyframes celebrate-bob {
  0%, 100% { transform: translateY(0) rotate(-6deg); }
  50%      { transform: translateY(-9px) rotate(6deg); }
}
.celebrate-card h2 { margin-block: 0 0.3rem; }
.celebrate-card p { color: var(--ink-soft); margin-block-end: 1.2rem; }

@media (prefers-reduced-motion: reduce) {
  .celebrate-veil, .celebrate-card, .celebrate-emoji { animation: none; }
  .confetti-layer { display: none; }
}

/* =========================================================================
   Leaderboard
   ========================================================================= */
.visually-hidden {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* How the board is ordered, said out loud. Deliberately NOT .muted: the line
   above it is a welcome and can be skimmed, this one answers "why is she above
   me", and a child who cannot find the answer decides there isn't a fair one. */
.lb-rule {
  margin-block: 0.5rem 0;
  font-size: 0.9rem;
  color: var(--ink);
  line-height: 1.7;
}

/* DOM order stays 1-2-3 for screen readers; `order` puts first place centre. */
.podium {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 0.6rem;
  list-style: none;
  margin: 1.5rem 0 2rem;
  padding: 0;
}
.podium-slot {
  flex: 1 1 0;
  max-inline-size: 190px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  text-align: center;
  animation: podium-rise 0.55s var(--ease-bounce) both;
}
.podium-1 { order: 2; animation-delay: 0.05s; }
.podium-2 { order: 1; animation-delay: 0.15s; }
.podium-3 { order: 3; animation-delay: 0.25s; }
@keyframes podium-rise {
  from { opacity: 0; transform: translateY(28px); }
  to   { opacity: 1; transform: none; }
}
.podium-medal { font-size: 2.3rem; line-height: 1; }
.podium-1 .podium-medal { font-size: 3rem; animation: celebrate-bob 2.6s ease-in-out infinite; }
.podium-name {
  font-weight: 800;
  font-size: 0.95rem;
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.podium-score { font-size: 0.86rem; font-weight: 800; color: var(--ink-soft); }
.podium-step {
  inline-size: 100%;
  border-radius: var(--r-md) var(--r-md) 0 0;
  background: linear-gradient(180deg, var(--brand-soft), var(--canvas-deep));
  border: 1.5px solid var(--line);
  border-block-end: 0;
}
.podium-1 .podium-step { block-size: 92px; background: linear-gradient(180deg, #ffe9b8, #ffd587); border-color: var(--sun); }
.podium-2 .podium-step { block-size: 68px; }
.podium-3 .podium-step { block-size: 52px; }
.podium-slot.is-me .podium-name { color: var(--brand-deep); }

.lb-table td, .lb-table th { vertical-align: middle; }
.lb-rank { inline-size: 3.2rem; text-align: center; font-weight: 800; }
/* Wider than the 4rem that held "100%": the whole course is 1460 points. */
.lb-score { inline-size: 5rem; text-align: end; font-weight: 800; font-variant-numeric: tabular-nums; }
.lb-me { background: var(--brand-soft); }
.lb-you {
  display: inline-block;
  margin-inline-start: 0.4rem;
  padding: 0.05rem 0.5rem;
  border-radius: var(--r-pill);
  background: var(--brand);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 800;
}

@media (max-width: 560px) {
  .podium-slot { max-inline-size: none; }
}

/* =========================================================================
   Report a problem
   ========================================================================= */
.bug-btn {
  border: 0;
  background: none;
  padding: 0;
  color: inherit;
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  opacity: 0.85;
}
.bug-btn:hover { opacity: 1; }

.bug-veil {
  position: fixed;
  inset: 0;
  z-index: 320;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: rgba(18, 16, 40, 0.5);
  backdrop-filter: blur(4px);
  animation: veil-in 0.18s var(--ease-out) both;
}
.bug-card {
  inline-size: min(460px, 100%);
  padding: 1.5rem;
  background: var(--paper);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  animation: celebrate-pop 0.32s var(--ease-bounce) both;
}
.bug-card h2 { margin-block-start: 0; }
.bug-card textarea {
  inline-size: 100%;
  min-block-size: 130px;
  padding: 0.7rem 0.85rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  resize: vertical;
}
.bug-card textarea:focus { outline: none; border-color: var(--brand); }
.bug-actions { display: flex; gap: 0.6rem; margin-block-start: 0.9rem; }
.bug-note { font-size: 0.84rem; margin-block: 0.5rem 0; }
.bug-note.is-error { color: var(--coral-deep); }
.bug-note.is-ok { color: var(--grass-deep); }

@media (prefers-reduced-motion: reduce) {
  .podium-slot, .bug-veil, .bug-card { animation: none; }
}

/* =========================================================================
   Inline staff editing
   A pen at the corner of the lesson text / exercise prompt, shown only to
   staff. It stays invisible until the block is hovered or something inside
   it takes focus, so the page a teacher reviews looks exactly like the page
   a student reads — until they reach for it.
   ========================================================================= */
.editable { position: relative; }

/* Belt and braces: [hidden] is a UA default that any later `display` rule on
   .card / .markdown-body would silently beat, leaving the rendered markdown
   stacked on top of the textarea. */
.editable [data-editable-view][hidden] { display: none; }

.edit-pen {
  position: absolute;
  inset-block-start: 0.4rem;
  inset-inline-end: 0.4rem;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.2rem;
  block-size: 2.2rem;
  padding: 0;
  border: 1.5px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transform: scale(0.9);
  transition: opacity var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.editable:hover > .edit-pen,
.editable:focus-within > .edit-pen,
.edit-pen:focus-visible {
  opacity: 1;
  transform: none;
}
.edit-pen:hover { border-color: var(--brand); background: var(--brand-soft); }

/* A touch screen has no hover, so the pen would never appear on a tablet. */
@media (hover: none) {
  .editable > .edit-pen { opacity: 1; transform: none; }
}

.editable-form { margin-block-start: 0.4rem; }
.editable-field {
  margin-block: 0 0.35rem;
  font-family: var(--ide-font);
  letter-spacing: 0.02em;
}
.editable-form textarea {
  inline-size: 100%;
  min-block-size: 12rem;
  resize: vertical;
  padding: 0.9rem 1rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ide-font);
  font-size: 0.92rem;
  line-height: 1.7;
  tab-size: 4;
}
.editable-form textarea:focus { border-color: var(--brand); outline: none; }

.editable-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-block-start: 0.7rem;
}
.editable-error {
  margin-block: 0.6rem 0;
  color: var(--coral-deep, var(--coral));
  font-weight: 700;
}

/* =========================================================================
   Staff dashboard — the teacher's view.
   Deliberately quieter than the student UI: dense tables and flat numbers
   instead of islands, rings and confetti. Same tokens, different volume.
   ========================================================================= */
.staff-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
}
.staff-head-text h1 { margin-block-end: 0.2rem; }
.staff-head-text p { margin-block-end: 0; }

.staff-tabs { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.staff-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 1rem;
  border: 1.5px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--ink-soft);
  font-weight: 800;
  font-size: 0.93rem;
  text-decoration: none;
  transition: background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out);
}
.staff-tab:hover { background: var(--brand-soft); color: var(--brand-deep); }
.staff-tab.is-active {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

/* --- headline numbers ---------------------------------------------------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
}
.stat {
  padding: 1.1rem 1.3rem;
  background: var(--paper);
  border: 1.5px solid var(--line);
  /* The accent sits on the inline-start edge, so it flips with the page
     direction instead of stranding itself on the wrong side in Persian. */
  border-inline-start: 6px solid var(--brand);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
.stat-grass { border-inline-start-color: var(--grass); }
.stat-sun   { border-inline-start-color: var(--sun); }
.stat-coral { border-inline-start-color: var(--coral); }
.stat-emoji { display: block; font-size: 1.4rem; line-height: 1; }
.stat-value {
  display: block;
  font-size: 2rem;
  font-weight: 900;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.stat-label { display: block; font-size: 0.85rem; font-weight: 700; color: var(--ink-soft); }

/* Inside a card the same numbers are supporting detail, not the headline. */
.stat-grid-quiet .stat {
  border: 0;
  border-radius: var(--r-md);
  box-shadow: none;
  background: var(--canvas-deep);
  padding: 0.8rem 1rem;
}
.stat-grid-quiet .stat-value { font-size: 1.55rem; }

.staff-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1rem;
}

/* Counts line up column-wise and never wrap mid-number. */
.staff-num { font-variant-numeric: tabular-nums; font-weight: 800; white-space: nowrap; }

/* --- search & pager ------------------------------------------------------ */
.staff-search { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.staff-search input[type="search"] {
  flex: 1 1 240px;
  min-inline-size: 0;
  padding: 0.6rem 1rem;
  border: 2px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  font: inherit;
  color: var(--ink);
}
.staff-search input[type="search"]:focus { border-color: var(--brand); outline: none; }

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.8rem;
}
.pager-link {
  padding: 0.45rem 1rem;
  border: 1.5px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  font-size: 0.9rem;
  font-weight: 800;
  text-decoration: none;
}
.pager-link:hover { background: var(--brand-soft); }
.pager-status { color: var(--ink-soft); font-size: 0.9rem; }

/* --- table extras -------------------------------------------------------- */
.staff-rowlink {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: inherit;
  font-weight: 700;
  text-decoration: none;
}
.staff-rowlink:hover { color: var(--brand-deep); }
.staff-rowlink small { display: block; font-weight: 600; }

/* A progress read that fits in a table cell — the student-facing ring is far
   too big here, so this is the same idea flattened. */
.bar-mini {
  display: inline-block;
  inline-size: 80px;
  block-size: 8px;
  vertical-align: middle;
  margin-inline-end: 0.45rem;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  overflow: hidden;
}
.bar-mini > i {
  display: block;
  block-size: 100%;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--grass), var(--sun));
}

/* Medal colours are not brand tokens — gold/silver/bronze have to look like
   metal, so these three are the only literals in this block. */
.grade-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.7rem;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  color: var(--ink-soft);
  font-size: 0.82rem;
  font-weight: 800;
  white-space: nowrap;
}
.grade-gold   { background: color-mix(in srgb, var(--sun) 14%, var(--paper)); color: var(--ink-sun); }
.grade-silver { background: color-mix(in srgb, var(--ink) 8%, var(--paper)); color: color-mix(in srgb, var(--ink-soft) 40%, var(--ink)); }
.grade-bronze { background: color-mix(in srgb, var(--coral) 12%, var(--paper)); color: var(--ink-coral); }

/* --- one student's page -------------------------------------------------- */
.staff-profile { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 1.2rem; }
.staff-profile-text { flex: 1 1 260px; min-inline-size: 0; }
.staff-profile-text .card-title { margin-block-end: 0.15rem; }
.staff-profile-ring { flex: 0 0 auto; text-align: center; }
.staff-profile-ring .cert-progress-ring {
  inline-size: 108px;
  block-size: 108px;
  margin-block: 0 0.3rem;
}
.staff-profile-ring .cert-progress-num { font-size: 1.4rem; }
.avatar-lg { inline-size: 54px; block-size: 54px; font-size: 1.5rem; }

.kv {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.6rem 1.2rem;
  margin-block: 0.9rem 0;
}
.kv dt {
  font-size: 0.76rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}
.kv dd { margin-inline-start: 0; font-weight: 800; }

.staff-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1.4rem; }
.staff-action { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.staff-action label { font-weight: 800; color: var(--ink-soft); }
/* Every text control in the panel, not just the number one. This named
   input[type="number"] alone, so the certificate-name box and the type-the-
   username-to-confirm box were raw browser inputs sitting next to properly
   styled buttons — square corners, hairline border, nothing like the rest of
   the site. */
.staff-action input[type="number"],
.staff-action input[type="text"],
.staff-action input:not([type]),
.staff-action select,
.staff-action textarea {
  padding: 0.55rem 0.75rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  font: inherit;
  color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
/* A number is two or three characters; a name or a username is not. */
.staff-action input[type="number"] { inline-size: 6.5rem; }
.staff-action input[type="text"],
.staff-action input:not([type]) { inline-size: min(20rem, 100%); }
.staff-action :is(input, select, textarea):focus {
  border-color: var(--brand);
  outline: none;
  box-shadow: 0 0 0 4px var(--brand-soft);
}

@media (max-width: 640px) {
  .staff-head { align-items: flex-start; }
  .stat-value { font-size: 1.7rem; }
  .bar-mini { inline-size: 56px; }
}

/* A "5 / 100" pair is one number, not two. Without an LTR isolate the bidi
   algorithm swaps the sides in Persian/Arabic/Urdu and a teacher reads the
   balance backwards. */
.ratio { direction: ltr; unicode-bidi: isolate; }

/* =========================================================================
   Profile
   ========================================================================= */
.prof-head {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin-block-end: 1.2rem;
}
.prof-avatar {
  inline-size: 68px;
  block-size: 68px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand);
  box-shadow: 0 var(--lift) 0 var(--brand-deep);
  color: #fff;
  font-weight: 900;
  font-size: 1.9rem;
}
.prof-id { flex: 1 1 12rem; min-inline-size: 0; }
.prof-id h1 { margin: 0; font-size: clamp(1.4rem, 1rem + 2vw, 2rem); }
.prof-id p { margin: 0; }

.prof-stats { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.prof-stat {
  min-inline-size: 92px;
  padding: 0.6rem 0.9rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  text-align: center;
}
.prof-stat b { display: block; font-size: 1.3rem; line-height: 1.1; }
.prof-stat b i { font-style: normal; font-size: 0.8rem; color: var(--ink-faint); }
.prof-stat small { color: var(--ink-soft); font-size: 0.76rem; }

.prof-progress { display: grid; gap: 0.5rem; justify-items: start; }
.prof-progress-bar {
  inline-size: 100%;
  block-size: 14px;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  overflow: hidden;
}
.prof-progress-bar i {
  display: block;
  block-size: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--grass), var(--sky));
  transition: inline-size var(--t-mid) var(--ease-out);
}

/* --- tidy tables --------------------------------------------------------- */
.table-wrap { overflow-x: auto; }
.tidy-table { inline-size: 100%; border-collapse: collapse; }
.tidy-table th, .tidy-table td {
  padding: 0.6rem 0.7rem;
  text-align: start;
  border-block-end: 1.5px solid var(--line);
}
.tidy-table th {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}
.tidy-table td.num, .tidy-table th.num { text-align: end; font-variant-numeric: tabular-nums; }
.tidy-table tr:last-child td { border-block-end: 0; }

.pill {
  display: inline-block;
  padding: 0.12rem 0.55rem;
  border-radius: var(--r-pill);
  font-size: 0.74rem;
  font-weight: 800;
  background: var(--canvas-deep);
  color: var(--ink-soft);
  white-space: nowrap;
}
.pill--concept  { background: #e3f2ff; color: #1069a8; }
.pill--debug    { background: #eaf8dd; color: #33790a; }
.pill--solution { background: #ffe6e6; color: #b32020; }

/* --- forms that don't look like raw Django ------------------------------- */
.tidy-form { display: grid; gap: 1rem; justify-items: start; }
.tidy-form .field { display: grid; gap: 0.3rem; inline-size: min(420px, 100%); }
.tidy-form label { font-weight: 800; font-size: 0.9rem; }
.tidy-form input[type="text"],
.tidy-form input[type="email"],
.tidy-form input[type="password"],
.tidy-form select,
.tidy-form textarea {
  inline-size: 100%;
  padding: 0.7rem 0.9rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  font: inherit;
  color: var(--ink);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.tidy-form select { cursor: pointer; }
.tidy-form :is(input, select, textarea):focus {
  outline: none;
  border-color: var(--sky);
  box-shadow: 0 0 0 4px rgba(28, 176, 246, 0.18);
}
.field-error { color: var(--coral-deep); font-weight: 700; font-size: 0.8rem; }

.empty--inline { display: grid; justify-items: center; gap: 0.3rem; padding-block: 0.6rem; }

/* Replay-the-tour button, sitting beside the close button in Pity's panel. */
.robot-tour {
  border: 0;
  background: transparent;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.25rem;
  border-radius: var(--r-sm);
  opacity: 0.85;
  transition: transform var(--t-fast) var(--ease-bounce), opacity var(--t-fast);
}
.robot-tour:hover { transform: rotate(-18deg) scale(1.12); opacity: 1; }

/* Staff reorder handles beside each challenge. */
.challenge-row { display: flex; align-items: center; gap: 0.4rem; }
.challenge-row .challenge { flex: 1 1 auto; }
.reorder { display: grid; gap: 2px; flex: none; }
.reorder-btn {
  inline-size: 26px;
  block-size: 20px;
  display: grid;
  place-items: center;
  border: 1.5px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  color: var(--ink-soft);
  font-size: 0.6rem;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.reorder-btn:hover:not(:disabled) { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-deep); }
.reorder-btn:disabled { opacity: 0.3; cursor: default; }

/* Difficulty pips on a challenge row — visible before it is attempted so a
   student can pick their level, not just admire what they already beat. */
/* The gap separates one difficulty from the next, so it has to be wider than
   the space between stars inside a group — otherwise a challenge offered at
   all three levels reads as a single run of six stars instead of ☆, ☆☆, ☆☆☆. */
.challenge-levels { display: inline-flex; gap: 0.5rem; margin-inline-start: auto; }
/* Stars count, they do not signal by colour. Difficulty is one, two or three
   stars, the way the Introduction explains it; hollow means not yet beaten,
   filled means beaten. One colour throughout, because three colours with no
   legend anywhere read as a code the child is expected to know. */
.lvl {
  font-size: 0.95rem;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--ink-faint);
  white-space: nowrap;
}
.lvl.is-solved { color: var(--sun-deep); }

/* =========================================================================
   Links — blue, no underline. Underlines everywhere made the lesson pages
   look like a wall of hyperlinks; weight and colour carry it instead, with
   the underline reserved for hover so the affordance is still discoverable.
   ========================================================================= */
a {
  color: var(--link);
  font-weight: 700;
  text-decoration: none;
}
a:hover,
a:focus-visible {
  color: var(--sky);
  text-decoration: none;
}
/* Things that are already buttons/cards must not inherit link colour. */
.btn, .btn:hover,
.nav-link, .nav-link:hover,
.isle-card, .isle-card:hover,
.stone, .stone:hover,
.brand, .brand:hover,
.user-chip, .credits-chip, .langpick-item {
  text-decoration: none;
}

/* =========================================================================
   Responsive navbar.
   The bar held brand + progress ring + 5 links + credits + user + language,
   which simply cannot fit a phone. Below 900px the links collapse into a
   disclosure menu; the ring, credits and language stay visible because they
   are the things a student actually glances at.
   ========================================================================= */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  inline-size: 42px;
  block-size: 42px;
  padding: 0;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  cursor: pointer;
  flex: none;
}
.nav-toggle span {
  display: block;
  inline-size: 18px;
  block-size: 2.5px;
  border-radius: 2px;
  background: var(--ink);
  box-shadow: 0 -6px 0 var(--ink), 0 6px 0 var(--ink);
}

@media (max-width: 900px) {
  /* One row, closed: mark, language, menu button. It used to be three rows
     stacked, because .langpick claimed flex-basis 100% and the toggle then
     wrapped again, which cost 165px, a fifth of a phone screen, before any
     content at all. */
  .nav { flex-wrap: wrap; gap: 0.4rem; row-gap: 0.4rem; align-items: center; }

  /* Two groups, one at each end: who you are and how far you have got on the
     side the language starts from, the controls on the other. The ring
     carried no order at all, so it defaulted to 0 and sat OUTSIDE the mark —
     the first thing on the bar was a percentage rather than Pity. */
  .brand { order: 1; }
  .nav-ring { order: 2; }
  /* Absorbs the leftover width on desktop; on a phone it would push each
     following item onto a row of its own, which is how the bar became four
     rows tall. */
  .nav-spacer { display: none; }
  .nav > .langpick { order: 3; margin-inline-start: auto; flex: 0 0 auto; }
  .nav-toggle { display: inline-flex; order: 4; margin-inline-start: 0; }
  .brand-name { display: none; }          /* the mark alone is enough here */

  /* Open: one item per line, not a wrapped grid of half-width chips. A list
     you scan down beats a mosaic you have to read across. */
  .nav-links {
    order: 10;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0.25rem;
    padding-block: 0.5rem 0.75rem;
    border-block-start: 1.5px solid var(--line);
  }
  /* Collapsed by default; the toggle flips aria-expanded, which drives this. */
  .nav-toggle[aria-expanded="false"] + .nav-links { display: none; }
  .nav-link, .nav-links .btn, .inline-form {
    inline-size: 100%;
    justify-content: flex-start;
    padding-block: 0.6rem;
  }
  /* The pair takes the row; the chips inside keep their own width, so the
     coin badge stays a badge and the name sits next to it rather than under. */
  .nav-identity { inline-size: 100%; gap: 0.4rem; }
  .nav-identity .credits-chip,
  .nav-identity .user-chip { inline-size: auto; flex: 0 0 auto; }
  .nav-identity .user-chip { flex: 1 1 auto; min-inline-size: 0; }
  .nav-links .inline-form .linklike { inline-size: 100%; text-align: start; }

  /* The picker is compact in the bar, and its menu is capped to the screen
     rather than stretched to whatever row it landed in. */
  .langpick > summary { padding: 0.4rem 0.6rem; }
  .langpick-name { max-inline-size: 6ch; }
  .langpick-menu { inline-size: min(260px, calc(100vw - 1.5rem)); }
}

@media (max-width: 560px) {
  :root { --nav-h: 58px; }
  .nav-ring { inline-size: 40px; block-size: 40px; }
  .page { inline-size: min(var(--page), 100% - 1.2rem); padding-block: 1.2rem 3rem; }
  h1 { font-size: clamp(1.45rem, 1.1rem + 2.4vw, 2rem); }
}

/* =========================================================================
   Helper bot — size modes and small screens.
   Small is the default so the panel never covers the lesson; large and full
   exist because reading a hint with a code block in a 320px box is miserable.
   ========================================================================= */
.robot-panel { transition: inline-size var(--t-mid) var(--ease-out), block-size var(--t-mid) var(--ease-out); }
.robot-panel.is-small { inline-size: 340px; block-size: min(460px, 70vh); }
.robot-panel.is-large { inline-size: min(560px, calc(100vw - 2rem)); block-size: min(680px, 82vh); }
.robot-panel.is-full {
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  max-block-size: none;
  border-radius: 0;
}
.robot-panel.is-full .robot-log { max-block-size: none; }

.robot-btn {
  inline-size: 30px;
  block-size: 30px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  opacity: 0.75;
}
.robot-btn:hover { opacity: 1; background: rgba(255, 255, 255, 0.16); }
.robot-btn[aria-pressed="true"] { opacity: 1; background: rgba(255, 255, 255, 0.26); }

/* On a phone the panel is always full screen: a floating 340px box on a
   360px viewport is just a broken-looking overlay. Keyed on a coarse pointer
   as well as width, because a phone whose browser reports a wider viewport (an
   in-app webview, a large phone) is still a phone, and the windowed sizes read
   as broken there — full screen is the only mode that belongs on a touch
   device. */
@media (max-width: 600px), (pointer: coarse) and (max-width: 1024px) {
  .robot-panel,
  .robot-panel.is-small,
  .robot-panel.is-large,
  .robot-panel.is-full {
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    max-block-size: none;
    border-radius: 0;
  }
  .robot-panel [data-robot-size] { display: none; }  /* nothing to choose */
  .robot-fab { inset-block-end: 1rem; inset-inline-end: 1rem; }
  /* The panel is the whole screen here. A scale-in would inset it by 2% on
     every edge, which reads as "does not fit" rather than as an animation —
     so it fades instead, and cannot end up anywhere except exactly the
     viewport. */
  .robot-panel,
  .robot-panel.is-small,
  .robot-panel.is-large,
  .robot-panel.is-full { animation-name: panel-fade; transform: none; }
}


/* =========================================================================
   Helper bot header + launcher, tightened.
   The three size controls were overflowing a narrow header because the row
   had no min-width floor and the title was free to eat all of it.
   ========================================================================= */
.robot-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.7rem;
  min-block-size: 52px;
}
.robot-head h3 {
  flex: 1 1 auto;
  min-inline-size: 0;          /* lets the title shrink instead of shoving */
  margin: 0;
  font-size: 0.98rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.robot-head-pity,
.robot-head-credits,
.robot-btn { flex: none; }

.robot-btn {
  inline-size: 30px;
  block-size: 30px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.18);
  color: #fff;
  font-size: 0.82rem;
  line-height: 1;
  cursor: pointer;
  transition: background var(--t-fast), transform var(--t-fast);
}
.robot-btn:hover { background: rgba(255, 255, 255, 0.34); }
.robot-btn:active { transform: translateY(1px); }
.robot-btn[hidden] { display: none; }   /* [hidden] loses to display:grid */

/* Very narrow: the credit chip is the first thing to go, then the title. */
@media (max-width: 420px) {
  .robot-head-credits { display: none; }
  .robot-head h3 { font-size: 0.9rem; }
}

/* =========================================================================
   Pity's launcher — a rounded rectangle, not a circle.
   ========================================================================= */
.robot-fab {
  position: fixed;
  inset-block-end: 1.4rem;
  inset-inline-end: 1.4rem;
  z-index: 80;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.95rem 0.5rem 0.6rem;
  border: 0;
  border-radius: 18px;
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-deep) 100%);
  box-shadow: 0 5px 0 var(--brand-deep), var(--shadow-md);
  color: #fff;
  font: inherit;
  font-weight: 800;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}
.robot-fab::after {
  content: attr(data-label);
  font-size: 0.92rem;
  white-space: nowrap;
}
.robot-fab:hover { transform: translateY(-2px); box-shadow: 0 7px 0 var(--brand-deep), var(--shadow-lg); }
.robot-fab:active { transform: translateY(5px); box-shadow: 0 0 0 var(--brand-deep); }
.robot-fab-pity { display: grid; place-items: center; line-height: 0; }
.robot-fab-pity .pity { --pity-size: 38px; }

/* A soft halo so she reads as "tap me" without a spinning ring. */
.robot-fab-pulse {
  position: absolute;
  inset: -4px;
  border-radius: 22px;
  background: var(--brand);
  opacity: 0.28;
  z-index: -1;
  animation: fab-halo 2.6s ease-in-out infinite;
}
@keyframes fab-halo {
  0%, 100% { transform: scale(1); opacity: 0.22; }
  50%      { transform: scale(1.06); opacity: 0.36; }
}

/* On a phone the label costs width the thumb wants; keep just Pity. */
@media (max-width: 600px) {
  .robot-fab { padding: 0.5rem 0.6rem; border-radius: 16px; }
  .robot-fab::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .robot-fab-pulse { animation: none; }
}


/* =========================================================================
   No underlines, anywhere, ever.
   Links are distinguished by colour and weight instead. This sits last on
   purpose: it is the rule that wins if any component ever reintroduces one.
   ========================================================================= */
a,
a:hover,
a:focus,
a:visited,
.linklike,
.linklike:hover,
.markdown-body a,
.markdown-body a:hover { text-decoration: none; }

/* =========================================================================
   Program output as a terminal window.
   The frame is the point: a bare <pre> reads as "some text on the page",
   a windowed black box reads as "this is what YOUR program said".
   ========================================================================= */
.term {
  margin-block-start: 0.9rem;
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  /* Terminals are LTR devices. Even in a Persian page the output of a Python
     program is left-aligned machine text. */
  direction: ltr;
}
.term-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0.5rem 0.8rem;
  background: #22223a;
}
.term-bar i {
  inline-size: 11px;
  block-size: 11px;
  border-radius: 50%;
  flex: none;
}
.term-bar i:nth-child(1) { background: #ff5f57; }
.term-bar i:nth-child(2) { background: #febc2e; }
.term-bar i:nth-child(3) { background: #28c840; }
.term-title {
  margin-inline-start: 0.5rem;
  color: #9a97b8;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.term-body {
  padding: 0.9rem 1rem;
  background: #12121f;
  min-block-size: 3.2rem;
  max-block-size: 340px;
  overflow: auto;
}
.term-out,
.term-err {
  margin: 0;
  font-family: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.9rem;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}
.term-out { color: #e6e6f0; }
/* Errors keep the ✗ affordance elsewhere; here the colour is supported by
   the leading marker, so it never carries the meaning alone. */
.term-err { color: #ff7b72; margin-block-start: 0.6rem; }
.term-err::before { content: "✗ "; font-weight: 800; }
.term-empty { margin: 0; color: #6e6b8a; font-size: 0.88rem; font-style: italic; }

/* =========================================================================
   Input box for Run
   ========================================================================= */
.run-input {
  margin-block-start: 1rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  overflow: hidden;
}
.run-input > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 0.9rem;
  background: var(--canvas-deep);
  font-weight: 800;
  cursor: pointer;
  list-style: none;
}
.run-input > summary::-webkit-details-marker { display: none; }
.run-input > summary::before {
  content: "⌨️";
  font-size: 1.05rem;
}
.run-input > summary::after {
  content: "▸";
  margin-inline-start: auto;
  color: var(--ink-faint);
  transition: transform var(--t-fast) var(--ease-out);
}
.run-input[open] > summary::after { transform: rotate(90deg); }
[dir="rtl"] .run-input[open] > summary::after { transform: rotate(-90deg); }
.run-input > summary small { font-weight: 600; color: var(--ink-soft); }

.run-input textarea {
  display: block;
  inline-size: 100%;
  border: 0;
  border-block-start: 2px solid var(--line);
  padding: 0.8rem 1rem;
  background: var(--paper);
  font-family: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.92rem;
  line-height: 1.7;
  resize: vertical;
}
.run-input textarea:focus { outline: none; background: color-mix(in srgb, var(--sun) 10%, var(--paper)); color: var(--ink); }
.run-input textarea::placeholder { color: var(--ink-faint); font-style: italic; }

/* ==========================================================================
   Signup — the first screen a student ever sees
   ========================================================================== */
.signup-wrap { max-inline-size: 480px; margin-inline: auto; }

.signup-hero { text-align: center; margin-block-end: 1.1rem; }
.signup-hero .pity { margin-inline: auto; }
.signup-title {
  font-size: clamp(1.5rem, 5vw, 2rem);
  font-weight: 900;
  margin-block: 0.3rem 0.2rem;
  color: var(--ink);
}
.signup-sub { color: var(--ink-soft); margin: 0; }

.signup-card { padding: 1.4rem 1.3rem 1.2rem; }

.signup-field { margin-block-end: 1.05rem; }
.signup-field > label {
  display: block;
  font-weight: 800;
  margin-block-end: 0.35rem;
  color: var(--ink);
}

/* The input and its trailing button share one rounded box, so the eye icon
   reads as part of the field rather than a control floating next to it. */
.signup-input { position: relative; }
/* input, select AND textarea. This used to name only `input`, so any form
   using this wrapper for a dropdown or a message box got a raw browser
   control next to properly styled text fields — which is exactly how the
   help-desk form looked. */
.signup-input input,
.signup-input select,
.signup-input textarea {
  inline-size: 100%;
  padding: 0.8rem 0.95rem;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--paper);
  font: inherit;
  font-size: 1.02rem;
  color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.signup-input input:focus,
.signup-input select:focus,
.signup-input textarea:focus {
  border-color: var(--brand);
  outline: none;
  box-shadow: 0 0 0 4px var(--brand-soft);
}

/* A message box should look like a message box: room to write, resizable
   downward only so it cannot be dragged wider than the card it sits in. */
.signup-input textarea {
  min-block-size: 8rem;
  resize: vertical;
  line-height: 1.7;
}
/* Native selects ignore most box styling until appearance is reset, which is
   why the dropdown kept its system look while the text fields did not. */
.signup-input select {
  appearance: none;
  /* The arrow is two 6px squares, each holding half of a triangle: the 45deg
     one is the LEFT half, the 135deg one the RIGHT half. They only read as a
     single ▼ if they sit exactly 6px apart — touching, never overlapping —
     and always in that order. Both invariants were broken: they were 4.8rem/16
     apart so they overlapped by 1.2px, and the RTL rule mirrored the positions
     without swapping the halves, which put the right half on the left and drew
     a bent little pennant instead of an arrow. Hence px, not rem, for the
     6px spacing: it has to match background-size exactly, and a rem is only
     16px until somebody changes the root size. */
  background-repeat: no-repeat;
  background-size: 6px 6px, 6px 6px;
  padding-inline-end: 2.2rem;
  cursor: pointer;
}
/* The arrow sits on whichever side the language reads towards — and the two
   halves keep their order, so the triangle points down in both. */
[dir="ltr"] .signup-input select {
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%),
                    linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
  background-position: right 20px center, right 14px center;
}
[dir="rtl"] .signup-input select {
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%),
                    linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
  background-position: left 14px center, left 20px center;
}
/* Username and email are typed, read back, and typed again at a login screen.
   A monospace-ish slab makes l/1 and O/0 distinguishable. */
.signup-field[data-name="username"] .signup-input input,
.signup-field[data-name="email"] .signup-input input {
  letter-spacing: 0.01em;
}
/* Latin identifiers stay LTR even on an RTL page. */
.signup-field[data-name="username"] .signup-input input,
.signup-field[data-name="email"] .signup-input input,
.signup-input input[type="password"] {
  direction: ltr;
  text-align: start;
}
[dir="rtl"] .signup-field[data-name="username"] .signup-input input,
[dir="rtl"] .signup-field[data-name="email"] .signup-input input,
[dir="rtl"] .signup-input input[type="password"] { text-align: right; }

.signup-field.has-error .signup-input input { border-color: var(--coral); }

/* Room for the eye button so long passwords do not run under it. */
.signup-field[data-name^="password"] .signup-input input { padding-inline-end: 3rem; }

.pw-eye {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-end: 0.5rem;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  inline-size: 2.2rem;
  block-size: 2.2rem;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
}
.pw-eye:hover { color: var(--ink-soft); background: var(--canvas-deep); }
.pw-eye .eye-slash { opacity: 0; transition: opacity 0.12s ease; }
.pw-eye.is-showing { color: var(--brand-deep); }
.pw-eye.is-showing .eye-slash { opacity: 1; }

/* --- strength meter ------------------------------------------------------
   Four bars, filled left to right. Not a security control: the server's
   validators decide what is allowed. This only nudges away from "12345678". */
.pw-meter {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-block-start: 0.45rem;
}
.pw-bars { display: flex; gap: 4px; flex: 1; }
.pw-bars i {
  block-size: 6px;
  flex: 1;
  border-radius: var(--r-pill);
  background: var(--line);
  transition: background 0.2s ease;
}
.pw-meter[data-score="1"] .pw-bars i.on { background: var(--coral); }
.pw-meter[data-score="2"] .pw-bars i.on { background: var(--sun); }
.pw-meter[data-score="3"] .pw-bars i.on { background: var(--sky); }
.pw-meter[data-score="4"] .pw-bars i.on { background: var(--brand); }
.pw-word { font-size: 0.82rem; font-weight: 800; color: var(--ink-soft); }
.pw-meter[data-score="1"] .pw-word { color: var(--coral-deep); }
.pw-meter[data-score="4"] .pw-word { color: var(--brand-deep); }

/* --- status lines --------------------------------------------------------
   One line per field. Empty ones collapse to nothing so the form does not
   jump around as it fills in. */
.signup-msg {
  font-size: 0.86rem;
  font-weight: 700;
  margin-block-start: 0.3rem;
}
/* Server-side errors: one line each, stacked. */
.signup-msg.is-bad > span { display: block; }
.signup-msg.is-bad { color: var(--coral-deep); }
/* The live line is a tick or a cross followed by text, on one row. Marking
   the mark as a flex item is what put it on its own line. */
.signup-msg.is-live { display: flex; align-items: baseline; gap: 0.3rem; }
.signup-msg.is-live.is-good { color: var(--brand-deep); }
.signup-msg.is-live.is-good::before { content: "✓"; }
.signup-msg.is-live.is-bad::before { content: "✕"; }
.signup-msg:empty,
.signup-msg.is-live:empty { display: none; }

.signup-field .helptext { margin-block-start: 0.3rem; line-height: 1.45; }

.signup-go { margin-block-start: 0.4rem; }

.signup-privacy {
  font-size: 0.8rem;
  color: var(--ink-faint);
  text-align: center;
  margin-block: 0.8rem 0;
  line-height: 1.5;
}

.signup-alt { text-align: center; margin-block-start: 1rem; color: var(--ink-soft); }
.signup-alt a { font-weight: 800; }

/* The captcha ships its own <img>; keep it from stretching to full width. */
/* The picture and its refresh button belong together on one line. */
.captcha-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-block-end: 0.5rem;
}
.signup-field[data-name="captcha"] .signup-input img,
.captcha-row img {
  border-radius: var(--r-sm);
  border: 2px solid var(--line);
  display: block;
  flex: 0 0 auto;
  /* Images are centred site-wide with an auto inline margin, which inside a
     flex row resolves to real space and pushed the button 96px away from the
     picture it belongs to. */
  margin: 0;
}
.signup-field[data-name="captcha"] .signup-input input[type="text"] {
  inline-size: 100%;
}
.pw-meter[data-score="0"] .pw-word { color: var(--coral-deep); }

/* A free answer says so rather than showing a minus zero, which reads as a
   bug. Green because nothing was spent. */
.robot-cost.is-free {
  color: var(--brand-deep);
  font-weight: 800;
  text-transform: lowercase;
}

/* What the helper costs, stated before anything is spent. Collapsed by
   default so it never competes with the conversation. */
.robot-prices {
  border-block-start: 1px solid var(--line);
  padding: 0.5rem 0.8rem;
  font-size: 0.8rem;
  color: var(--ink-soft);
}
.robot-prices summary {
  cursor: pointer;
  font-weight: 800;
  color: var(--ink);
  list-style: none;
}
.robot-prices summary::-webkit-details-marker { display: none; }
.robot-prices summary::before { content: "🪙 "; }
.robot-prices ul { margin: 0.5rem 0 0.4rem; padding-inline-start: 0; list-style: none; }
.robot-prices li { display: flex; gap: 0.45rem; padding-block: 0.12rem; }
.robot-prices li b { min-inline-size: 3.6rem; color: var(--ink); }
.robot-prices p { margin: 0; font-size: 0.76rem; }

/* A row of buttons. Two inline .btn links in a <p> are separated only by the
   whitespace between them, and that whitespace collapses at a line break, so
   they ended up touching once they wrapped. A flex gap survives wrapping. */
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  align-items: center;
}
.hero .btn-row { margin-block-start: 1.1rem; }
/* Stacked on a phone, each button takes the full width: a half-width button
   next to a wrapped one looks like a mistake. */
@media (max-width: 520px) {
  .btn-row { gap: 0.6rem; }
  .btn-row > .btn { inline-size: 100%; }
}

/* The six digit code. Wide, spaced and centred: it is copied by eye from an
   email, and grouped characters are far easier to check than a run of six. */
.code-input {
  inline-size: 100%;
  padding: 0.9rem 1rem;
  border-radius: var(--r-md);
  border: 2px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 1.9rem;
  font-weight: 900;
  letter-spacing: 0.5em;
  text-align: center;
  text-indent: 0.5em;  /* balances the trailing letter-space */
  direction: ltr;
}
.code-input:focus {
  border-color: var(--brand);
  outline: none;
  box-shadow: 0 0 0 4px var(--brand-soft);
}

.verify-resend {
  display: flex;
  justify-content: center;
  gap: 1.2rem;
  margin-block-start: 1rem;
  font-size: 0.9rem;
}
.linkish {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 800;
  color: var(--sky-deep);
  cursor: pointer;
}
.linkish:disabled { color: var(--ink-faint); cursor: default; }

/* A reorder the server refused. Shakes rather than reloading, so "nothing
   moved" is visibly different from "it moved". */
.is-refused { animation: refuse-shake 0.35s ease; }
@keyframes refuse-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

/* The one irreversible action in the staff panel. Set apart and last, so it
   is never the thing a tired hand reaches for by accident. */
.danger-zone {
  border: 2px solid var(--coral);
  /* Derived from --coral and --paper rather than a hex literal, so a re-tint
     of the palette carries this card with it. */
  background: color-mix(in srgb, var(--coral) 6%, var(--paper));
}
.danger-zone .card-title { color: var(--coral-deep); }
.btn-danger { --btn-face: var(--coral); --btn-edge: var(--coral-deep); --btn-text: #fff; }

/* Pity asleep: the model is unreachable or busy. Deliberately not red. The
   child did nothing wrong and there is nothing for them to fix. */
.robot-msg.is-asleep {
  background: var(--canvas-deep);
  color: var(--ink-soft);
  border: 1px dashed var(--line);
  font-weight: 700;
}
.robot-msg.is-asleep::before { content: "😴 "; }

/* Pity waiting at the end of an island trail, where a lone red flag used to
   sit. The flag read as a warning marker; she reads as an invitation. */
.trail-goal-pity {
  display: block;
  margin-inline: auto;
  inline-size: fit-content;
}

/* The QR on a certificate. Sized in absolute units because this is printed:
   a code smaller than about 22mm is unreliable on a phone camera. */
.cert-qr {
  margin-block-start: 1.4rem;
  text-align: center;
}
.cert-qr-svg {
  inline-size: 108px;
  block-size: 108px;
  display: inline-block;
}
.cert-qr-note {
  font-size: 0.72rem;
  color: var(--ink-faint);
  margin-block-start: 0.35rem;
}
@media print {
  .cert-qr-svg { inline-size: 26mm; block-size: 26mm; }
  /* Never invert or tint a QR: scanners need real black on real white.
     Targeted, not a blanket `* { fill: #000 }`. The modules are drawn as a
     stroked path and the quiet zone as a filled one, so a blanket fill did
     nothing to the modules and would now paint the white background black,
     printing a solid square. */
  .cert-qr-svg path[stroke] { stroke: #000 !important; }
  .cert-qr-svg path[fill] { fill: #fff !important; }
  /* Browsers drop background colours when printing unless asked not to. */
  .cert-qr-svg { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* Sign in with Google. Google's brand guidelines require their mark, their
   wording and a neutral surface, so this button deliberately does not take
   the site's colours. */
.oauth-block { margin-block-start: 1.1rem; }
/* Google now sits ABOVE the form, so the rule follows the button instead of
   preceding it: its job is to separate Google from the fields BELOW. The
   margin has to move with it, or the "or" hugs the form and drifts away from
   the button it belongs to. */
.oauth-block-top { margin-block-start: 0; margin-block-end: 0; }
.oauth-block-top .oauth-or { margin-block: 0.9rem 0; }
.oauth-or {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  color: var(--ink-faint);
  font-size: 0.82rem;
  margin-block-end: 0.8rem;
}
/* Signup's form IS the card, so Google gets a card of its own directly above
   it rather than floating unanchored on the page background. */
.oauth-card { margin-block-end: 1rem; padding-block-end: 0.25rem; }
.oauth-card .oauth-or { margin-block: 0.9rem 0.35rem; }
.oauth-or::before,
.oauth-or::after {
  content: "";
  flex: 1;
  block-size: 1px;
  background: var(--line);
}
.btn-google {
  --btn-face: #ffffff;
  --btn-edge: #dadce0;
  --btn-text: #3c4043;
  border: 1px solid #dadce0;
  gap: 0.65rem;
  text-transform: none;
  letter-spacing: 0;
}
.btn-google:hover { --btn-face: #f8f9fa; }

/* =========================================================================
   Judge results

   Every child sees this on their first submission, so it is the screen the
   whole judging pipeline exists to produce. The rules for it were lost when
   the frontend was rebuilt: the class names stayed in editor.js, the CSS did
   not, and pass and fail rendered identically as plain body text with raw
   <br><small> inside. Rewritten in the current palette rather than restored,
   because the old one referenced variables (--ok, --surface, --radius) that
   no longer exist.
   ========================================================================= */
.score-big {
  font-size: 2.6rem;
  font-weight: 900;
  line-height: 1;
  display: inline-block;
  vertical-align: -0.1em;
}
.score-good { color: var(--brand-deep); }
.score-mid { color: var(--sun-deep); }
.score-low { color: var(--coral-deep); }

.test-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin-block: 1rem;
}

/* One tile per test. Colour is never the only signal: each carries a tick or
   a cross, so it still reads for a colour-blind child and in print. */
.test-badge {
  border-radius: var(--r-md);
  padding: 0.5rem 0.85rem;
  font-weight: 800;
  font-size: 0.85rem;
  line-height: 1.35;
  border: 2px solid var(--line);
  background: var(--paper);
  color: var(--ink);
}
.test-badge small {
  display: block;
  font-weight: 700;
  opacity: 0.8;
  /* Error types are Latin identifiers; keep them LTR inside RTL prose. */
  unicode-bidi: isolate;
}
.test-pass {
  border-color: var(--brand);
  background: var(--brand-soft);
  color: var(--brand-deep);
}
.test-fail {
  border-color: var(--coral);
  background: color-mix(in srgb, var(--coral) 10%, var(--paper));
  color: var(--ink-coral);
}
.test-pending { color: var(--ink-faint); }

/* Sample stdout, folded away until asked for. */
[data-results] details {
  margin-block-start: 0.6rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  padding: 0.5rem 0.8rem;
}
[data-results] summary { cursor: pointer; font-weight: 800; font-size: 0.85rem; }
[data-results] details pre {
  margin-block-start: 0.5rem;
  background: var(--ide-bg);
  color: var(--ide-text);
  padding: 0.7rem 0.9rem;
  border-radius: var(--r-sm);
  overflow-x: auto;
  direction: ltr;
  text-align: start;
}

/* ==========================================================================
   Staff monitor
   ========================================================================== */
.mon-grid { display: grid; gap: 1rem; }
.mon-tiles {
  display: grid;
  gap: 0.8rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.mon-tile {
  padding: 1rem;
  border-radius: var(--r-lg);
  border: 2px solid var(--line);
  background: var(--paper);
  text-align: center;
}
/* A number worth walking over to look at. Deliberately not red: the queue
   being busy is normal during a class, it is only worth noticing. */
.mon-tile.is-hot { border-color: var(--sun); background: color-mix(in srgb, var(--sun) 12%, var(--paper)); }
.mon-value { font-size: 2rem; font-weight: 900; color: var(--ink); line-height: 1.1; }
.mon-label { font-size: 0.78rem; color: var(--ink-soft); margin-block-start: 0.2rem; }

.mon-rows { margin: 0; }
.mon-rows > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.32rem;
  border-block-end: 1px solid var(--line);
}
.mon-rows > div:last-child { border-block-end: 0; }
.mon-rows dt { color: var(--ink-soft); font-size: 0.88rem; }
.mon-rows dd { margin: 0; font-weight: 800; font-variant-numeric: tabular-nums; }

.mon-list { list-style: none; padding: 0; margin: 0; }
.mon-list li {
  display: flex;
  justify-content: space-between;
  padding-block: 0.3rem;
  border-block-end: 1px solid var(--line);
}
.mon-list li:last-child { border-block-end: 0; }
.mon-list b { font-variant-numeric: tabular-nums; }

.mon-sub { font-size: 0.72rem; color: var(--ink-faint); margin-block-start: 0.25rem; }
.mon-value small { font-size: 0.5em; font-weight: 700; color: var(--ink-soft); }
/* Broken, as opposed to busy. .is-hot means "worth noticing"; this one means
   somebody is not getting what they came for. */
.mon-tile.is-bad {
  border-color: var(--coral);
  background: color-mix(in srgb, var(--coral) 10%, var(--paper));
}
.mon-tile.is-bad .mon-value { color: var(--ink-coral); }

/* The refreshed block is only a wrapper for the poll to swap; its children
   belong to .mon-grid, so it must not become a grid item itself. */
.mon-live { display: contents; }

/* ---- charts -------------------------------------------------------------
   Drawn by ECharts (static/js/monitor.js) into these boxes from raw numbers.
   The box owns the SIZE; ECharts fills it. A fixed block-size is required
   because an auto-height grid/flex child would collapse the chart's absolutely
   positioned canvas to nothing. */
.mon-echart {
  inline-size: 100%;
  block-size: 220px;
  margin-block-start: 0.6rem;
  /* Pinned LTR so a time axis is laid out left-to-right whatever the page
     direction — "now" stays on the right-hand edge for a Persian reader too.
     The one chart that mirrors (failures, whose axis is a list of names) is
     flipped INSIDE ECharts from its rtl flag, not by the page flipping it. */
  direction: ltr;
}
/* The failures chart is horizontal bars; monitor.js sizes it to the number of
   categories, so this is only the height before any data has arrived. */
.mon-echart--fails { block-size: 160px; }

/* An empty chart says so in words. A flat line at zero is the shape of a quiet
   hour AND the shape of an event store that stopped writing. */
.mon-nodata {
  margin-block-start: 0.6rem;
  padding: 1.1rem;
  border: 2px dashed var(--line);
  border-radius: var(--r-md);
  text-align: center;
  color: var(--ink-faint);
  font-size: 0.85rem;
}
.mon-nodata--good { color: var(--ink-grass); border-color: var(--brand); }

/* The three numbers above the Judge0 bars. A row on a wide screen and a
   column on a phone, because "today / this month / busiest" is read as a set
   and wrapping one of them onto its own line breaks that. */
.mon-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
  margin-block: 0.6rem 0.9rem;
  font-size: 0.85rem;
  color: var(--ink-faint);
}
.mon-figures b {
  color: var(--ink);
  font-size: 1.05rem;
}

/* The lesson-by-lesson report on a student's page. Folded per island, because
   a supporter wants one open while they talk to a child and a hundred lessons
   flat is a page nobody reads. */
.lesson-rows > td { padding-block-start: 0; }
.lesson-report,
.lesson-challenges { margin: 0.4rem 0 0; padding-inline-start: 1.1rem; list-style: none; }
.lesson-report > li { margin-block-end: 0.45rem; }
.lesson-challenges { margin-block-start: 0.2rem; }
.lesson-tick { margin-inline-end: 0.35rem; }
.lesson-rows summary { cursor: pointer; }

/* ---- what is failing ---------------------------------------------------- */
.mon-fails { list-style: none; padding: 0; margin: 0.9rem 0 0; }
.mon-fail {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-block: 0.4rem;
  border-block-end: 1px solid var(--line);
}
.mon-fail:last-child { border-block-end: 0; }
.mon-fail-n {
  min-inline-size: 2.4rem;
  font-variant-numeric: tabular-nums;
  font-weight: 900;
  color: var(--ink-sun);
}
.mon-fail--bad .mon-fail-n { color: var(--ink-coral); }
.mon-fail-say { flex: 1 1 16rem; color: var(--ink); font-size: 0.9rem; }
.mon-fail-cat {
  font-size: 0.7rem;
  color: var(--ink-faint);
  direction: ltr;
  unicode-bidi: isolate;
}

/* The concrete cases under a failure: which url, which field, which exception.
   flex-basis 100% so they start on their own line under the sentence rather
   than trailing off the end of it. */
.mon-cases {
  flex: 1 0 100%;
  list-style: none;
  margin: 0.25rem 0 0;
  padding-inline-start: 2.9rem;   /* lines up under the sentence, past the count */
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 1.1rem;
  font-size: 0.78rem;
  color: var(--ink-soft);
}
.mon-cases li { display: flex; align-items: baseline; gap: 0.35rem; }
.mon-cases b { font-variant-numeric: tabular-nums; color: var(--ink); }
.mon-cases code { font-size: 0.72rem; color: var(--ink-faint); }
.mon-case-rest { font-style: italic; color: var(--ink-faint); }

/* ---- the live tail ------------------------------------------------------ */
.mon-tail {
  list-style: none;
  padding: 0;
  margin: 0.6rem 0 0;
  max-block-size: 26rem;
  overflow-y: auto;
}
.mon-ev {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.3rem 0.5rem;
  border-inline-start: 3px solid var(--line);
  border-block-end: 1px solid var(--line);
  font-size: 0.8rem;
}
.mon-ev:last-child { border-block-end: 0; }
.mon-ev--good { border-inline-start-color: var(--brand); }
.mon-ev--warn { border-inline-start-color: var(--sun); }
.mon-ev--bad {
  border-inline-start-color: var(--coral);
  background: color-mix(in srgb, var(--coral) 7%, var(--paper));
}
.mon-ev-when { color: var(--ink-faint); min-inline-size: 7rem; }
.mon-ev-kind {
  color: var(--ink-soft);
  font-weight: 800;
  min-inline-size: 4.5rem;
  direction: ltr;
  unicode-bidi: isolate;
}
.mon-ev-what { flex: 1 1 12rem; color: var(--ink); }
.mon-ev-ms { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.mon-ev-who { color: var(--link); unicode-bidi: isolate; }

@media (min-width: 780px) {
  .mon-grid { grid-template-columns: 1fr 1fr; }
  .mon-tiles { grid-column: 1 / -1; }
  /* A chart of sixty minutes in half a column is unreadable. */
  .mon-wide { grid-column: 1 / -1; }
}


/* Between the phone menu and a wide desktop there is a band where a signed-in
   staff account has more links than fit. Tighten them rather than letting the
   bar set the page width: a page that scrolls sideways on a laptop is a bug
   on every single screen, not just this one. */
@media (min-width: 901px) and (max-width: 1180px) {
  .nav-links { gap: 0.15rem; }
  .nav-link { padding-inline: 0.5rem; font-size: 0.92rem; }
  .nav-credits, .nav-user { font-size: 0.88rem; }
}

/* Between the phone menu and a roomy desktop, the labels step aside and the
   emoji carries the link.
   Measured: an English staff bar needs 1087px of the 1120px container, so
   every laptop between the 900px menu breakpoint and ~1120px wrapped "Log
   out" onto a second line. Hiding five labels gives back roughly 260px, which
   covers every language — and unlike a hard-coded padding tweak it does not
   depend on how long any particular translation happens to be.
   The label is CLIPPED, not display:none, so it stays in the accessibility
   tree and a screen reader still announces "Leaderboard"; sighted users get
   the title attribute as a tooltip. */
@media (min-width: 901px) and (max-width: 1180px) {
  .nav-links .nav-label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .nav-links .nav-link { padding-inline: 0.5rem; }
  .nav-links .nav-emoji { font-size: 1.15rem; }
  /* Headroom. At the bottom of this tier the bar had 4px to spare in English,
     and the language picker and a display name are both wider in some of the
     other fifteen — 4px is not a margin, it is luck. */
  .nav { gap: 0.4rem; }
  .nav-links { gap: 0.15rem; }
  .user-chip-name { max-inline-size: 6ch; }
  .langpick-name { max-inline-size: 8ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* Set by app.js when it has MEASURED that the bar does not fit. A breakpoint
   cannot decide this: .container caps at 1120px, so how much room the links
   need depends on the language, not the screen. German needs ~1112px of that
   1120 and wraps at every viewport width; English needs ~1087 and fits. The
   script compares the two numbers and only then hides the labels. */
.nav--compact .nav-links .nav-label {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.nav--compact .nav-links .nav-link { padding-inline: 0.5rem; }
.nav--compact .nav-links .nav-emoji { font-size: 1.15rem; }
.nav--compact .user-chip-name { max-inline-size: 6ch; }
.nav--compact .langpick-name {
  max-inline-size: 8ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* If the links will not fit, they WRAP. They used to scroll sideways with the
   scrollbar hidden, which is the same as being cut off: adding the Help link
   pushed a staff bar 19px over budget and "Log out" was simply gone, with
   nothing on screen to say it could be scrolled to.
   The earlier note here was right that tuning padding alone is a fix that
   breaks the next time somebody adds a link — so padding is only the thing
   that keeps the common case on one row, and wrapping is what catches every
   case it does not. Nothing is ever hidden either way. */
@media (min-width: 901px) {
  .nav-links {
    overflow: visible;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.2rem;
    /* Pills have rounded ends; without a little room they clip at the edge. */
    padding-inline: 2px;
  }
  .nav-links .nav-link { padding-inline: 0.6rem; }
  .nav { gap: 0.55rem; }
}

/* The picker sits beside .nav-links, not inside it, so its dropdown is not
   clipped by that element's horizontal scrolling. See base.html. */
/* Desktop only. This sat outside any media query and therefore came AFTER
   the mobile block, quietly replacing the auto margin that pushes the
   language picker and the menu button to the far end of a phone bar — so the
   two groups never reached the two edges. */
@media (min-width: 901px) {
  .nav > .langpick { margin-inline-start: 0.5rem; }
}

/* ==========================================================================
   Lesson scratchpad + "try it" on code blocks
   ========================================================================== */
/* "Try it" tucks into the code box's top-right corner. The box (.highlight) is
   overflow:hidden with a 14px radius, so a corner-pinned tab is clipped to the
   box's own rounded corner and reads as part of it — only its inner corner is
   rounded. The code gets a little headroom so the tab never sits on the first
   line. */
.code-wrap { position: relative; }
.markdown-body .highlight .code-wrap pre { padding-block-start: 2.6rem; }
.code-tryit {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  padding: 0.32rem 0.8rem;
  border: 0;
  border-end-start-radius: 12px;
  background: #223049;
  color: #cbd5e1;
  font: 800 0.73rem/1.4 var(--ui-font);
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.code-tryit:hover, .code-tryit:focus-visible { background: var(--sky); color: #fff; }

.scratch {
  margin-block: 1.4rem;
  border: 2px solid var(--line);
  border-radius: var(--r-md);
  background: var(--canvas-deep);
  overflow: hidden;
}
.scratch > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 0.9rem;
  font-weight: 800;
  cursor: pointer;
  list-style: none;
}
.scratch > summary::-webkit-details-marker { display: none; }
.scratch-body { padding: 0 0.9rem 0.9rem; }
.scratch-note { font-size: 0.82rem; color: var(--ink-soft); margin-block: 0 0.6rem; }

/* --- first-time editor introduction --------------------------------------
   A child who has never seen an editor cannot act on "try some code". These
   four blocks answer, in order: what is this, what does running do, what does
   it look like when it works, and what exactly do I press. The demo screen
   borrows the editor's own dark surface so the worked example and the real
   editor a few centimetres below read as the same object. */
.ed-intro { margin-block-end: 0.9rem; }
.ed-intro-lead { font-weight: 800; margin: 0 0 0.25rem; }
.ed-intro-how { font-size: 0.86rem; color: var(--ink-soft); margin: 0 0 0.8rem; line-height: 1.65; }
.ed-intro-now { font-weight: 700; font-size: 0.9rem; margin: 0.9rem 0 0.5rem; }

.ed-demo-cap { font-size: 0.8rem; color: var(--ink-soft); margin: 0 0 0.35rem; }
.ed-demo-screen {
  /* Permanently dark, like the editor and terminal it imitates — not a
     surface that follows the page. */
  background: #171728;
  border: 1px solid #262640;
  border-radius: var(--r-md);
  padding: 0.7rem 0.9rem;
  overflow-x: auto;
}
.ed-demo-code, .ed-demo-out {
  margin: 0;
  font: 500 0.85rem/1.7 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre;
}
.ed-demo-code { color: #d6d6f5; }
.ed-demo-out {
  color: #9fe6a0;
  margin-block-start: 0.5rem;
  padding-block-start: 0.5rem;
  border-block-start: 1px dashed #33334f;
  /* Empty until the program "runs", so the rule above would otherwise draw a
     stray line across an empty box before the demo starts. */
  min-block-size: 1.2em;
}
.ed-demo.is-typing .ed-demo-code::after {
  content: "▋";
  color: var(--brand);
  animation: ed-caret 0.9s step-end infinite;
}
@keyframes ed-caret { 50% { opacity: 0; } }

.ed-demo-replay {
  margin-block-start: 0.45rem;
  padding: 0.15rem 0.5rem;
  border: 0;
  background: none;
  color: var(--sky);
  font: 700 0.78rem/1 var(--ui-font);
  cursor: pointer;
  border-radius: var(--r-sm);
}
.ed-demo-replay:hover { text-decoration: underline; }
/* Nothing to replay until it has played once. */
.ed-demo:not(.is-done) .ed-demo-replay { visibility: hidden; }

.ed-starters { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.ed-starter {
  padding: 0.4rem 0.75rem;
  border: 2px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--ink);
  font: 700 0.85rem/1 var(--ui-font);
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.ed-starter:hover { border-color: var(--brand); }
.ed-starter.is-active {
  border-color: var(--brand);
  background: var(--brand-soft);
  color: var(--ink-grass);
}
@media (prefers-reduced-motion: reduce) {
  .ed-demo.is-typing .ed-demo-code::after { animation: none; }
}
.scratch-code, .scratch-stdin {
  inline-size: 100%;
  padding: 0.7rem 0.8rem;
  border: 2px solid var(--ide-line);
  border-radius: var(--r-sm);
  background: var(--ide-bg);
  color: var(--ide-text);
  font: 0.92rem/1.6 var(--ide-font);
  resize: vertical;
}
.scratch-code:focus, .scratch-stdin:focus { outline: none; border-color: var(--sky); }
.scratch-actions { display: flex; gap: 0.5rem; margin-block-start: 0.6rem; }
.scratch-out { margin-block-start: 0.8rem; }
.scratch-out.is-bad { color: var(--coral-deep); font-weight: 700; }
.scratch-out.is-busy { color: var(--ink-soft); }

/* Ask for a different captcha picture. */
.captcha-refresh {
  flex: 0 0 auto;
  /* Matches the picture's height so the two read as one control rather than
     a button that happens to be nearby. */
  align-self: stretch;
  inline-size: 2.6rem;
  block-size: auto;
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--paper);
  color: var(--ink-soft);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  vertical-align: middle;
}
.captcha-refresh:hover { border-color: var(--brand); color: var(--brand-deep); }
.captcha-refresh.is-spinning { animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Where to go after a challenge. */
.ex-next {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin-block-start: 1.4rem;
}
@media (max-width: 520px) { .ex-next > .btn { inline-size: 100%; } }

/* The coin and the profile read as one unit: a person, and what they have.
   They were separated by the nav's gap, which made the coin look like a
   third navigation item. */
/* No negative margin. This used to pull the two together against the nav's
   own gap; once they moved into .nav-identity there was no gap left to pull
   against, so the coin pill slid under the name pill and the two visibly
   overlapped. The wrapper's gap is the only thing that spaces them now. */

/* Coins and name are one unit. On desktop the wrapper changes nothing; in the
   open mobile menu it is what stops each of them claiming a full-width row of
   its own, which turned a two-digit coin count into a banner. */
.nav-identity { display: inline-flex; align-items: center; gap: 0.4rem; flex: 0 0 auto; }

/* A long display name pushed the rest of the bar off screen. Clip it; the
   full name is on the profile page it links to. */
.user-chip-name {
  max-inline-size: 9ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* A name is read from its start. In an RTL bar a Latin name clipped from
     the left instead, so "Bartholomew Fitzgerald-Smythe" showed as
     "…ld-Smythe" and the child could not recognise themselves. Isolating the
     text lets it keep its own direction inside a bar that has another. */
  unicode-bidi: isolate;
  text-align: start;
}
@media (max-width: 900px) { .user-chip-name { max-inline-size: 14ch; } }

/* The admins list on the staff users page. Names and a link, deliberately no
   e-mail column: the student table below is where addresses belong, and this
   card sits on a page a teacher leaves open on a projector. */
.admin-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
}
.admin-list li { display: flex; align-items: baseline; gap: 0.35rem; }

/* Submission verdicts. One shape, three tones: waiting, good, bad — plus a
   warning for the case where the fault is ours rather than the child's. The
   word carries the meaning; the colour only reinforces it, so this still
   reads correctly in greyscale or to a colour-blind child. */
.verdict {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 800;
  line-height: 1.5;
  white-space: nowrap;
}
.verdict--good { background: color-mix(in srgb, var(--grass) 16%, transparent); color: var(--grass-deep, var(--grass)); }
.verdict--bad  { background: color-mix(in srgb, var(--coral) 16%, transparent); color: var(--coral); }
.verdict--warn { background: color-mix(in srgb, var(--sun) 22%, transparent); color: var(--sun-deep); }
/* "Waiting to be judged" is a sentence a child reads as STUCK. The pill runs a
   slow sweep across itself instead, which reads as WORKING — the same thing a
   progress bar says when it does not know how long it will take. The sweep is
   the element's own background, not an overlay, so the label stays on top of
   it and queue.js can keep rewriting the text without touching any of this. */
.verdict--wait {
  background-color: var(--canvas-deep);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    color-mix(in srgb, var(--brand) 32%, transparent) 50%,
    transparent 100%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  color: var(--ink-soft);
  animation: verdict-sweep 1.4s ease-in-out infinite;
}
/* Sweeps along the reading direction, so it moves the way the eye already is. */
[dir="rtl"] .verdict--wait { animation-direction: reverse; }

@keyframes verdict-sweep {
  from { background-position: -110% 0; }
  to   { background-position: 210% 0; }
}


.queue-table td { vertical-align: middle; }
.pager { display: flex; align-items: center; gap: 0.8rem; margin-block-start: 0.9rem; }

/* A ticket thread. The staff reply is tinted so a child can see at a glance
   which message is the answer they were waiting for, without reading the
   byline first. */
.ticket-msg { border-inline-start: 4px solid var(--line); }
.ticket-msg.is-staff {
  border-inline-start-color: var(--brand);
  background: color-mix(in srgb, var(--brand) 5%, var(--paper));
}

/* Count of tickets waiting on you. Small, and only drawn when non-zero — a
   badge showing 0 is a permanent worry about nothing. */
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 1.15rem;
  padding: 0 0.3rem;
  margin-inline-start: 0.3rem;
  border-radius: var(--r-pill);
  background: var(--coral);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 900;
  line-height: 1.5;
}

/* The profile is one column. A two-column grid was tried and made the page
   worse: the cards were different heights, so it read as a ragged mosaic
   rather than a page, and the badges — the thing a child actually opens this
   page for — ended up in a half-width box off to one side. One column, full
   width, badges in the middle of it. */
.prof-stack { margin-block-start: 1.1rem; }
.prof-stack > .card { inline-size: 100%; }

/* The verification page's facts.
   Not .cert-grid: that lays out three short fields across a printed
   certificate, and its 140px columns wrapped this page's sentence mid-phrase
   ("…پای‌تیچ را کامل ·" / "کرد") and left the separator stranded on its own
   line. Here each fact gets the full width of the card on a phone and half of
   it once there is room, with real space between rows. */
.cert-facts {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem 1.75rem;
  margin-block-start: 1.5rem;
  padding-block-start: 1.4rem;
  border-block-start: 2px solid var(--canvas-deep);
  text-align: start;
}
@media (min-width: 560px) {
  .cert-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.cert-fact { min-inline-size: 0; }
.cert-facts .cert-field-label { margin-block-end: 0.3rem; }
.cert-facts .cert-field-value {
  font-size: 1.02rem;
  line-height: 1.7;
  /* Values mix scripts — a Latin name, a serial, a score — inside a page that
     may be RTL. Isolating each one stops it dragging the punctuation around
     it to the wrong end. */
  unicode-bidi: isolate;
  overflow-wrap: break-word;
}
/* The separator travels with the score rather than floating between two
   directions, and never begins a line. */
.cert-sep { margin-inline: 0.15rem; color: var(--ink-faint); white-space: nowrap; }
.cert-facts .cert-serial { display: inline-block; unicode-bidi: isolate; }

/* ==========================================================================
   Coming soon — the whole site before release day
   ========================================================================== */
.soon-wrap {
  display: grid;
  justify-items: center;
  gap: 1.1rem;
  text-align: center;
  max-inline-size: 34rem;
  margin-inline: auto;
  padding-block: clamp(2rem, 6vw, 4.5rem) 4rem;
}
.soon-badge {
  inline-size: clamp(180px, 42vw, 260px);
  block-size: auto;
  filter: drop-shadow(0 10px 28px rgba(45, 190, 110, 0.35));
}
.soon-title {
  margin: 0;
  font-size: clamp(1.9rem, 1.2rem + 3vw, 2.9rem);
  line-height: 1.2;
}
.soon-lede { margin: 0; color: var(--ink-soft); font-size: 1.05rem; line-height: 1.75; }

/* Telegram's own blue: this is a link off our site, and recolouring somebody
   else's mark to ours makes it harder to recognise, not more on-brand. */
.soon-tg {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1.5rem;
  border-radius: var(--r-pill);
  background: #127bb5;
  color: #fff;
  font-weight: 800;
  text-decoration: none;
  box-shadow: 0 var(--lift) 0 #1e8bc3;
  transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast);
}
.soon-tg:hover { color: #fff; transform: translateY(-2px); box-shadow: 0 6px 0 #1e8bc3; }
.soon-tg:active { transform: translateY(var(--lift)); box-shadow: 0 0 0 #1e8bc3; }
.soon-note { margin: 0; color: var(--ink-faint); font-size: 0.9rem; }
/* The recommendation runs longer than the old one-liner, so it needs a
   readable measure rather than a single centred line stretched across. */
.soon-note--rec {
  max-inline-size: 46ch;
  margin-inline: auto;
  line-height: 1.7;
}
.soon-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; justify-content: center; }

/* =========================================================================
   "Is this your email?" — read back before the account is made
   ========================================================================= */
/* A typo in the address is unrecoverable: it is how a forgotten password comes
   home, and it is unique, so the wrong one cannot be claimed again later. */
.emailcheck {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.2rem;
  background: rgba(0, 0, 0, 0.45);
}
.emailcheck[hidden] { display: none; }
.emailcheck-box {
  background: var(--paper);
  border-radius: var(--r-lg, 18px);
  border: 1.5px solid var(--line);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
  padding: 1.6rem 1.4rem 1.3rem;
  max-inline-size: 26rem;
  inline-size: 100%;
  text-align: center;
}
.emailcheck-emoji { font-size: 2.6rem; line-height: 1; display: block; }
.emailcheck-box h2 { margin-block: 0.5rem 0.7rem; font-size: 1.25rem; }
/* dir="ltr" on the element keeps an address readable inside a Persian or
   Arabic page; this centres it there rather than pinning it to one edge. */
.emailcheck-value {
  font-weight: 700;
  font-size: 1.05rem;
  word-break: break-all;
  background: var(--canvas-deep);
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm, 10px);
  padding: 0.6rem 0.7rem;
  margin-block-end: 0.7rem;
  text-align: center;
}
.emailcheck-why {
  color: var(--ink-soft);
  font-size: 0.9rem;
  line-height: 1.7;
  margin-block-end: 1rem;
}
.emailcheck-actions { display: grid; gap: 0.55rem; }
/* The page behind must not scroll under the dialog. */
body.has-modal { overflow: hidden; }

/* One flat price, said once, in the panel rather than behind a disclosure. */
.robot-price-note {
  margin: 0;
  padding: 0.55rem 0.9rem;
  border-block-start: 1.5px solid var(--line);
  background: var(--canvas-deep);
  color: var(--ink-soft);
  font-size: 0.84rem;
  line-height: 1.7;
  text-align: center;
}

.scratch-title { display: flex; align-items: center; gap: 0.6rem; margin: 0 0 0.9rem; font-size: 1.05rem; }
.ed-demo-show { margin-block: 0.2rem 0.8rem; }

/* An editable heading keeps the heading's own size; only the pen is new. */
.editable-title { position: relative; }
.editable-title h1 { margin-block: 0; }

/* =========================================================================
   Course editor — the whole course as one tree, plus a form page per thing.
   Logical properties throughout: the panel is read in Persian and Arabic as
   often as in English, and the tree's indentation has to flip with them.
   ========================================================================= */
.tree-note { margin: 0; }

.tree-chapter { padding: 0.9rem 1rem; }
.tree-chapter.is-draft { border-style: dashed; }

.tree-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  border-radius: var(--r-md);
}
.tree-row:hover { background: var(--canvas-deep); }
.tree-row--chapter { font-size: 1.05rem; }
.tree-icon { flex: 0 0 auto; font-size: 1.05rem; }

/* The name takes the slack so every row's buttons line up on the same edge. */
.tree-name {
  flex: 1 1 12rem;
  min-inline-size: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-weight: 800;
}
.tree-name > a { color: inherit; text-decoration: none; }
.tree-name > a:hover { color: var(--brand-deep); text-decoration: underline; }
.tree-name small { font-weight: 600; }
.tree-peek { font-weight: 600; }

.tree-row--chapter .tree-name { font-weight: 900; }
.tree-row--item { font-size: 0.93rem; }
.tree-row--item .tree-name { font-weight: 700; }

/* Depth is a rule down the inside edge, not indentation alone: at three
   levels on a phone, indentation runs out of screen before it runs out of
   tree. */
.tree-lessons,
.tree-items {
  margin-inline-start: 0.9rem;
  padding-inline-start: 0.8rem;
  border-inline-start: 2px solid var(--line);
}
.tree-lessons { margin-block-start: 0.3rem; }
.tree-items { margin-block: 0.2rem 0.5rem; }
.tree-lesson + .tree-lesson { margin-block-start: 0.5rem; }

/* Difficulty variants of one challenge are one row on the student's page, so
   here they are one box — otherwise the arrows, which move all three, look
   like they moved the wrong thing. */
.tree-group.is-variants {
  border-inline-start: 3px solid var(--brand-soft);
  border-radius: var(--r-sm, 10px);
  margin-block: 0.2rem;
}

.tree-flag {
  padding: 0.1rem 0.6rem;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  color: var(--ink-faint);
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.is-draft > .tree-row .tree-name > a,
.tree-row--item.is-draft .tree-name > a { color: var(--ink-soft); }

.tree-acts { display: flex; align-items: center; gap: 0.3rem; flex: 0 0 auto; }
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2rem;
  block-size: 2rem;
  padding: 0;
  border: 1.5px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  color: var(--ink-soft);
  font-size: 0.95rem;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
}
.icon-btn:hover { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-deep); }

.tree-adds { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.4rem 0 0; }
.tree-add {
  padding: 0.25rem 0.7rem;
  border: 1.5px dashed var(--line);
  border-radius: var(--r-pill);
  color: var(--ink-soft);
  font-size: 0.82rem;
  font-weight: 800;
  text-decoration: none;
}
.tree-add:hover { border-color: var(--brand); border-style: solid; color: var(--brand-deep); }
.tree-empty { margin: 0.3rem 0; color: var(--ink-faint); font-size: 0.88rem; }

.pill {
  padding: 0.12rem 0.6rem;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  color: var(--ink-soft);
  font-size: 0.75rem;
  font-weight: 800;
  white-space: nowrap;
}
.pill-easy   { background: color-mix(in srgb, var(--grass) 16%, var(--paper)); color: var(--grass-deep); }
.pill-medium { background: color-mix(in srgb, var(--sun) 20%, var(--paper)); color: var(--ink-sun); }
.pill-hard   { background: color-mix(in srgb, var(--coral) 16%, var(--paper)); color: var(--coral-deep); }
/* Work already submitted. It is on the row so that "unpublish, don't delete"
   is a decision you make before opening the page, not after. */
.pill-work { background: color-mix(in srgb, var(--sky) 18%, var(--paper)); color: var(--sky-deep); }

.tree-orphans { border-color: var(--coral); }

/* --- the form pages ------------------------------------------------------ */
.editor-page .card + .card { margin-block-start: 1rem; }
.sub-title { margin-block: 1.4rem 0.3rem; font-size: 1rem; }

.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0 1rem;
}
.field textarea { min-block-size: 3rem; }
.field-check { display: flex; flex-direction: column; justify-content: flex-end; }
.field-check label { display: flex; align-items: center; gap: 0.5rem; font-weight: 800; }
.field-check input { inline-size: auto; }
.field.has-error input,
.field.has-error select,
.field.has-error textarea { border-color: var(--coral); }
/* Code is code in every language: never mirrored, never a proportional font. */
.code-box, input.num-sm {
  direction: ltr;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.field input.num-sm { inline-size: 5rem; }

/* One row of boxes, one per language. Equal width on purpose — an empty
   Arabic box the same size as a full Persian one is a gap you can see. */
.lang-set {
  margin-block-end: 1rem;
  padding: 0.7rem 0.9rem;
  border: 1.5px solid var(--line);
  border-radius: var(--r-md);
}
.lang-set legend {
  padding-inline: 0.4rem;
  font-size: 0.78rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}
.lang-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 0.9rem;
}
.lang-grid .field { margin-block-end: 0.6rem; }
.lang-grid label { font-size: 0.82rem; color: var(--ink-soft); }

.choice-list, .test-list { display: grid; gap: 0.7rem; margin-block-end: 1rem; }
.choice-card, .test-card {
  padding: 0.8rem 0.9rem;
  border: 1.5px solid var(--line);
  border-radius: var(--r-md);
  background: var(--canvas-deep);
}
.choice-card.has-error, .test-card.has-error { border-color: var(--coral); }
.choice-head, .test-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0 1rem;
}
.choice-head .field, .test-head .field { margin-block-end: 0.6rem; }
.choice-n, .test-n {
  align-self: center;
  font-size: 1.1rem;
  font-weight: 900;
  color: var(--ink-faint);
  margin-block-end: 0.6rem;
}
.test-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0 0.9rem; }
/* Dimmed, not hidden: the other pair may already hold text from before the
   kind was changed, and hiding it would look like the text was thrown away. */
.test-pair.is-idle { opacity: 0.45; }

/* --- the delete panel ---------------------------------------------------- */
.danger-card { border-color: var(--coral); }
.impact { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.35rem; }
.impact-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; font-size: 0.92rem; }
.impact-row.is-nil { opacity: 0.5; }
.impact-gone .staff-num { color: var(--coral-deep); }
.impact-erased .staff-num { color: var(--ink-sun); }
.impact-detached .staff-num { color: var(--sky-deep); }
.impact-block {
  margin: 0;
  padding: 0.8rem 1rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--coral) 10%, var(--paper));
  color: var(--ink);
  line-height: 1.8;
}
.confirm-row code {
  padding: 0.1rem 0.4rem;
  border-radius: var(--r-sm, 8px);
  background: var(--canvas-deep);
}

@media (max-width: 640px) {
  .tree-lessons, .tree-items { margin-inline-start: 0.3rem; padding-inline-start: 0.5rem; }
  .tree-acts { inline-size: 100%; justify-content: flex-end; }
}

/* --- one editor at a time ------------------------------------------------ */
.lock-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1rem;
  padding: 0.85rem 1.1rem;
  border: 2px solid var(--sun);
  border-radius: var(--r-lg, 18px);
  background: color-mix(in srgb, var(--sun) 12%, var(--paper));
}
.lock-hand { font-size: 1.4rem; line-height: 1; }
.lock-text { flex: 1 1 16rem; min-inline-size: 0; }
.lock-text p { margin: 0; line-height: 1.7; }
.lock-text p + p { margin-block-start: 0.3rem; }

/* A read-only form should look read-only before anybody clicks into it. */
.is-locked-form fieldset[disabled] { opacity: 0.72; }
/* The browser's own fieldset chrome would draw a box around the whole form;
   the fieldset is here to disable, not to group. */
.editor-page fieldset { min-inline-size: 0; margin: 0; padding: 0; border: 0; }

.lock-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.12rem 0.6rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sun) 22%, var(--paper));
  color: var(--ink-sun);
  font-size: 0.75rem;
  font-weight: 800;
  white-space: nowrap;
}

/* A control the server would refuse should not look ready to be pressed. */
.tree-acts :disabled,
.icon-btn.is-held {
  opacity: 0.4;
  cursor: not-allowed;
}
.icon-btn.is-held:hover {
  background: var(--paper);
  border-color: var(--line);
  color: var(--ink-soft);
}

/* Site-wide "you solved it" toast (static/js/app.js). Bottom-centre so it
   never sits under the navbar or the helper bot, and celebratory without
   blocking whatever the child moved on to. */
.solve-toast {
  position: fixed;
  inset-block-end: max(1.1rem, env(safe-area-inset-bottom));
  inset-inline: 0;
  margin-inline: auto;
  inline-size: fit-content;
  max-inline-size: min(92vw, 30rem);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.8rem 1.15rem;
  background: var(--paper);
  border: 2px solid var(--grass);
  border-radius: var(--r-pill);
  box-shadow: 0 8px 26px rgba(20, 22, 30, 0.16), 0 var(--lift) 0 var(--grass-deep);
  animation: solve-toast-in 0.34s var(--ease-out, cubic-bezier(0.2, 0.8, 0.3, 1));
}
.solve-toast.is-leaving { animation: solve-toast-out 0.32s ease forwards; }
.solve-toast-emoji { font-size: 1.5rem; line-height: 1; }
.solve-toast-text { font-weight: 800; color: var(--ink); line-height: 1.35; }
@keyframes solve-toast-in {
  from { transform: translateY(1.2rem) scale(0.96); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@keyframes solve-toast-out {
  to { transform: translateY(0.8rem); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .solve-toast, .solve-toast.is-leaving { animation: none; }
}

/* =========================================================================
   Waiting, and reading back what you sent.
   ========================================================================= */

/* Run's answer lands in the terminal pane, so the waiting belongs there too.
   An empty box for two seconds reads as "nothing happened" and a child
   presses Run again. Indeterminate on purpose: the judge cannot say how far
   along it is, and a bar that pretends to know is a bar that lies. */
.run-wait {
  block-size: 8px;
  inline-size: 100%;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-soft) 22%, transparent);
  overflow: hidden;
}
.run-wait-fill {
  display: block;
  block-size: 100%;
  inline-size: 40%;
  border-radius: inherit;
  background: var(--brand);
  animation: run-wait-slide 1.1s ease-in-out infinite;
}
@keyframes run-wait-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
/* The animation is decoration. Somebody who asked for less motion still needs
   to know the thing is working, so the bar stays and simply breathes. */
@media (prefers-reduced-motion: reduce) {
  .run-wait-fill {
    inline-size: 100%;
    animation: run-wait-breathe 1.8s ease-in-out infinite;
  }
  @keyframes run-wait-breathe {
    0%, 100% { opacity: 0.35; }
    50%      { opacity: 1; }
  }
}

/* The submitted program, read back in the same terminal frame it ran in. It
   carries .term as well, so the colours come from there and cannot drift. */
.code-peek {
  margin: auto;            /* .term sets a top margin; a dialog centres instead */
  padding: 0;
  border: none;
  border-radius: var(--r-md);
  inline-size: min(720px, 92vw);
  max-block-size: 82vh;
  overflow: hidden;
  background: #12121f;
  box-shadow: 0 18px 60px rgb(0 0 0 / 45%);
}
.code-peek::backdrop { background: rgb(0 0 0 / 55%); }
/* Taller than the inline output pane: this is a whole program, not one run's
   worth of printing. Not wrapped either — indentation is Python's grammar,
   and a wrapped line reads as a different one. */
.code-peek-body {
  max-block-size: 62vh;
  min-block-size: 0;
}
.code-peek-body .term-out { white-space: pre; }
.code-peek-foot {
  padding: 0.7rem;
  background: #22223a;
  text-align: center;
}
.queue-code-btn { white-space: nowrap; }

/* The editor's status line doubles as its error line. A refusal in the same
   grey as "Queued…" is a refusal a child reads straight past, and this is the
   only place the editor ever says no. */
.helptext.is-error {
  color: var(--coral);
  font-weight: 700;
}
