/* =========================================================================
   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;

  --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:       #b9e4f8;
  --sea-line:  #1f7ba8;
  --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;
}

/* 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-flag { font-size: 1.15rem; line-height: 1; }
.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 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);
  margin-block-end: 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.
   ========================================================================= */
@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; }
.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;
}

.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);
}
.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;
  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;
}

.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;
  letter-spacing: -0.02em;
}
.nav-ring-value i { font-style: normal; font-size: 0.56rem; opacity: 0.65; }
.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;
}

/* 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-pct { font-size: 0.86rem; 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; }
.lb-pct  { inline-size: 4rem; text-align: end; font-weight: 800; }
.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;
}
.lb-bar {
  display: block;
  block-size: 7px;
  inline-size: min(220px, 100%);
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  overflow: hidden;
}
.lb-bar i {
  display: block;
  block-size: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--brand), var(--sky));
}

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

/* =========================================================================
   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); }
}

/* --- Run input ----------------------------------------------------------- */
.run-input { margin-block-start: 0.8rem; }
.run-input summary {
  cursor: pointer;
  font-weight: 700;
  color: var(--ink-soft);
  padding-block: 0.35rem;
}
.run-input summary small { font-weight: 500; color: var(--ink-faint); }
.run-input textarea {
  inline-size: 100%;
  margin-block-start: 0.4rem;
  padding: 0.6rem 0.75rem;
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  font: 0.92rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  resize: vertical;
}
.run-input textarea:focus { border-color: var(--sky); outline: none; }

/* =========================================================================
   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. */
@media (max-width: 600px) {
  .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 -------------------------------------------------------------
   Hand-drawn inline SVG: a strict CSP blocks every chart library, and sixty
   numbers do not need one. Everything that can live in CSS does, so no locale
   ever gets the chance to write a comma into a stroke-dasharray. */
.mon-chart {
  inline-size: 100%;
  block-size: auto;
  display: block;
  margin-block-start: 0.6rem;
  overflow: visible;
  /* Pinned LTR so text-anchor start/end mean exactly what health.py assumed
     when it placed every label. The failure chart mirrors itself in Python;
     letting the browser mirror it a second time here is how a mirrored chart
     becomes nonsense. */
  direction: ltr;
}
.mon-axis-line { stroke: var(--line); stroke-width: 1; }
.mon-axis-text { fill: var(--ink-faint); font-size: 11px; font-weight: 700; }

.mon-bar { fill: var(--sky); }
.mon-bar:hover { fill: var(--sky-deep); }

/* Told apart by weight AND dash, not by colour alone: these three lines lie on
   top of each other for most of a healthy hour, and the reader has to be able
   to see which is which exactly when they separate. */
.mon-line { fill: none; stroke-linejoin: round; stroke-linecap: round; }
.mon-line--p50 { stroke: var(--brand-deep); stroke-width: 1.6; }
.mon-line--p95 { stroke: var(--sun-deep); stroke-width: 2; stroke-dasharray: 6 3; }
.mon-line--p99 { stroke: var(--coral); stroke-width: 2.4; stroke-dasharray: 2 3; }
.mon-dot--p50 { fill: var(--brand-deep); }
.mon-dot--p95 { fill: var(--sun-deep); }
.mon-dot--p99 { fill: var(--coral); }

.mon-legend {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  font-size: 0.78rem;
  color: var(--ink-soft);
}
.mon-legend li { display: flex; align-items: center; gap: 0.35rem; }
.mon-swatch { inline-size: 16px; block-size: 3px; border-radius: 2px; }
.mon-swatch--p50 { background: var(--brand-deep); }
.mon-swatch--p95 { background: var(--sun-deep); }
.mon-swatch--p99 { background: var(--coral); }

.mon-cat { fill: var(--ink); font-size: 13px; font-weight: 700; }
.mon-cat--n { fill: var(--ink-soft); font-variant-numeric: tabular-nums; }
.mon-fbar--bad { fill: var(--coral); }
.mon-fbar--warn { fill: var(--sun); }

/* 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); }

/* ---- 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 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
   ========================================================================== */
.code-wrap { position: relative; }
.code-tryit {
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-end: 0.55rem;
  padding: 0.22rem 0.6rem;
  border: 1px solid #33334f;
  border-radius: var(--r-sm);
  background: #21213a;
  color: #d6d6f5;
  font: 800 0.74rem/1.4 var(--ui-font);
  cursor: pointer;
  opacity: 0.75;
  transition: opacity var(--t-fast), background var(--t-fast);
}
.code-tryit:hover, .code-tryit:focus-visible { opacity: 1; background: var(--sky-deep); 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); }
.verdict--wait { background: var(--canvas-deep); color: var(--ink-soft); }

.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; }
