/* Design tokens. The single source for the dark arcade identity.
   Specified in docs/ARCHITECTURE.md section 8. */
:root {
  --bg: #0f1117;
  --surface: #1a1d29;
  --surface-raised: #141720;
  --editor: #12141d;
  --line: #2a2f3e;
  --line-strong: #3a4152;

  --text: #e8eaf0;
  --text-dim: #b9bfd0;
  --muted: #8b91a7;
  --muted-on-fill: #9ba2b8;

  --accent: #a3e635;
  --accent-soft: rgba(163, 230, 53, 0.14);
  --accent-glow: rgba(163, 230, 53, 0.5);
  --pet: #f472b6;
  --danger: #f87171;

  --radius-card: 16px;
  --radius-control: 10px;
  --dur: 160ms;
  --ease: cubic-bezier(0.2, 0, 0, 1);

  --font-ui: Inter, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
}

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

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.6;
}

.mono {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

/* Shared icons ----------------------------------------------------------- */

/* Inlined by lexipawspy.icons. Deliberately nothing here about fill or
   stroke: the outline controls are drawn in currentColor so they take
   the colour of whatever they sit in, and the six coloured resources
   carry their own palette in their vector layers. A blanket
   `.icon path { fill }` rule would flatten all six into one shape. */
.icon {
  display: inline-block;
  vertical-align: middle;
  flex-shrink: 0;
}

/* Top bar ---------------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 20px;
  min-height: 68px;
  padding: 0 28px;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--line);
}

.wordmark {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}

.xp {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 999 1 240px;
  min-width: 0;
}

.xp__level {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  font-size: 14px;
  font-weight: 600;
}

.xp__track {
  flex: 1 1 auto;
  min-width: 0;
  height: 8px;
  border-radius: 4px;
  background: var(--line);
  overflow: hidden;
}

.xp__fill {
  display: block;
  height: 100%;
  border-radius: 4px;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
  transition: width var(--dur) var(--ease);
}

.xp__label {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.pet {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}

.pet__state {
  color: var(--muted);
  font-size: 12px;
}

/* Shell ------------------------------------------------------------------ */

.shell {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 20px;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 28px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 32px;
}

.card--centred {
  margin: auto;
  max-width: 560px;
  text-align: center;
}

.eyebrow {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

h1 {
  margin: 0 0 14px;
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
}

.lede {
  margin: 0 0 20px;
  color: var(--text-dim);
}

.meta {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
}

/* Utility --------------------------------------------------------------- */

/* The `!important` is load-bearing and stays. x-cloak hides an element
   until Alpine has initialised, so it has to beat whatever display the
   element otherwise carries - without it, multi-file editors and the
   board flash their contents before Alpine runs. */
[x-cloak] {
  display: none !important;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.rule {
  height: 1px;
  margin: 20px 0;
  border: 0;
  background: var(--line);
}

.hint {
  margin: 0;
  padding: 16px 20px;
  color: var(--muted);
  font-size: 13.5px;
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: 12px;
}

/* Pet ------------------------------------------------------------------- */

.pet__label--thinking {
  display: none;
}

#pet.htmx-request .pet__label--thinking {
  display: inline;
}

#pet.htmx-request .pet__label--state {
  display: none;
}

#pet.htmx-request .pet__face {
  animation: pet-pulse 900ms var(--ease) infinite;
}

@keyframes pet-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

/* Buttons --------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur) var(--ease);
}

.button:hover {
  background: var(--line);
}

.button--primary {
  border: none;
  background: var(--accent);
  color: var(--bg);
  font-weight: 600;
  box-shadow: 0 0 14px rgba(163, 230, 53, 0.28);
}

.button--primary:hover {
  background: #b4ef4f;
}

/* While a submission is in flight.

   `hx-disabled-elt` puts this on the button that was pressed, and it
   is the only acknowledgement the learner gets where they are already
   looking. The pet in the top bar thinks too, but on a long lesson the
   pet is scrolled off the screen - which is how somebody came to press
   Submit, see nothing move, and conclude it was broken. */
.button:disabled {
  cursor: progress;
  opacity: 0.6;
  box-shadow: none;
}

.button:disabled:hover {
  background: var(--surface);
}

.button--primary:disabled:hover {
  background: var(--accent);
}

/* Badges and chips ------------------------------------------------------ */

.badges {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 16px;
}

.badge,
.chip {
  padding: 4px 10px;
  border-radius: 6px;
  background: var(--line);
  color: var(--text);
  font-size: 12px;
  font-weight: 500;
}

.badge--muted,
.chip {
  color: var(--muted-on-fill);
  font-weight: 400;
}

.badge--accent,
.chip--accent {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}

.concept-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
}

.concept-row:last-child {
  margin-bottom: 0;
}

.concept-row__label {
  width: 56px;
  color: var(--muted);
  font-size: 12px;
}

/* Lesson layout --------------------------------------------------------- */

.lesson {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  align-items: flex-start;
}

.lesson__brief {
  flex: 1 1 380px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.lesson__work {
  flex: 999 1 560px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.card--tight {
  padding: 20px 24px;
}

.card--flush {
  padding: 0;
  overflow: hidden;
}

.prose p {
  margin: 0 0 14px;
  color: var(--text-dim);
}

.prose p:last-child {
  margin-bottom: 0;
}

.prose code {
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--editor);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.88em;
}

/* Editor ---------------------------------------------------------------- */

.tabs {
  display: flex;
  gap: 4px;
  padding: 0 12px;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--line);
}

.tab {
  padding: 12px 14px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 13px;
  cursor: pointer;
}

.tab--active {
  color: var(--text);
  border-bottom-color: var(--accent);
}

.code-area {
  display: block;
  width: 100%;
  min-height: 320px;
  padding: 18px;
  border: 0;
  background: var(--editor);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 13.5px;
  line-height: 1.75;
  resize: vertical;
}

.code-area:focus {
  outline: none;
}

.code-area.is-replaced {
  display: none;
}

.cm-editor {
  height: 320px;
  font-size: 13.5px;
}

.cm-editor.cm-focused {
  outline: none;
}

.code-readonly {
  margin: 0;
  padding: 18px;
  min-height: 240px;
  background: var(--editor);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 13.5px;
  line-height: 1.75;
  overflow-x: auto;
}

.code-readonly__note {
  margin: 0;
  padding: 10px 18px;
  color: var(--muted);
  font-size: 12px;
  background: var(--editor);
  border-top: 1px solid var(--line);
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 14px 18px;
  background: var(--surface-raised);
  border-top: 1px solid var(--line);
}

.toolbar__spacer {
  flex: 1 1 auto;
}

.toolbar__meta {
  color: var(--muted);
  font-size: 12px;
}

/* Output and checks ----------------------------------------------------- */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
}

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 18px;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--line);
}

.panel__title {
  color: var(--muted);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.panel__meta {
  color: var(--muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.panel__meta--pass {
  color: var(--accent);
  font-weight: 500;
}

.panel__meta--fail {
  color: var(--danger);
  font-weight: 500;
}

.panel__body {
  margin: 0;
  padding: 18px;
  min-height: 84px;
  background: var(--editor);
  font-family: var(--font-mono);
  font-size: 13.5px;
  line-height: 1.7;
  white-space: pre-wrap;
  /* `word-break: break-word` did this and is deprecated. A learner's
     program can print one very long unbroken line, and the panel has
     to wrap it rather than scroll sideways. */
  overflow-wrap: break-word;
}

.panel__empty {
  color: var(--muted);
}

.stream-error {
  color: var(--danger);
}

.checks {
  margin: 0;
  padding: 0;
  list-style: none;
}

.check {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 15px 20px;
  border-bottom: 1px solid #22262f;
}

.check--fail {
  background: rgba(248, 113, 113, 0.07);
}

.check__icon {
  flex: 0 0 auto;
  margin-top: 1px;
}

.check--pass .check__icon {
  color: var(--accent);
}

.check--fail .check__icon {
  color: var(--danger);
}

.check__text {
  flex: 1 1 auto;
  min-width: 0;
}

.check__label {
  margin: 0;
  font-size: 14.5px;
}

.check__detail {
  margin: 6px 0 0;
  color: var(--danger);
  font-family: var(--font-mono);
  font-size: 13px;
}

.checks__verdict {
  margin: 0;
  padding: 14px 20px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.6;
  background: var(--surface-raised);
}

.checks__verdict--pass {
  color: var(--accent);
  font-weight: 500;
}

/* Map -------------------------------------------------------------------- */

.map {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.map__head h1 {
  margin: 0 0 6px;
}

.map__head .lede {
  margin: 0;
}

.chapter {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 24px;
}

.chapter--locked {
  border-style: dashed;
  background: transparent;
}

.chapter__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin: 0 0 22px;
}

.chapter__title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.chapter__count {
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* A grid rather than wrapped flex, so every dot in a row sits at the
   same height and the track below can run straight through them. The
   column gap is zero and the breathing room is padding inside each
   node, which is what keeps the track unbroken across a row. */
.nodes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  column-gap: 0;
  row-gap: 26px;
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

.node {
  position: relative;
  padding: 0 6px;
}

/* The track. It runs the full width of the cell at the dots' centre
   line, so adjacent cells join into one continuous path. */
.node::before {
  content: "";
  position: absolute;
  top: 28px;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--line-strong);
  opacity: 0.55;
}

/* Walked already: the path behind a passed step is lit. */
.node--passed::before {
  background: var(--accent);
  opacity: 0.4;
}

.node__link {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  color: inherit;
  text-decoration: none;
}

.node__dot {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  transition: transform 160ms var(--ease);
}

.node__link:hover .node__dot {
  transform: translateY(-2px);
}

.node__dot--passed {
  /* The check inside is drawn in currentColor, so this is what makes
     it dark against the filled disc. */
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 16px rgba(163, 230, 53, 0.3);
}

.node__dot--current {
  border: 2px solid var(--accent);
  box-shadow: 0 0 18px rgba(163, 230, 53, 0.35);
}

/* A lesson added behind the learner. Open, so it is not dashed and
   faded like a locked one, but quieter than the step they are actually
   on: no glow, because it is worth a look rather than the way
   forward. */
.node__dot--new {
  border: 1px solid var(--line-strong);
  background: var(--surface-raised);
}

.node__dot--new .node__pip {
  background: var(--text-dim);
}

.node__badge {
  display: block;
  width: fit-content;
  margin: 5px auto 0;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Unopened steps are circles on the path and nothing more. The words
   that used to sit under each one are in the lede now. */
.node__dot--locked {
  border: 1px dashed var(--line-strong);
  background: transparent;
  width: 34px;
  height: 34px;
  margin: 12px auto;
  opacity: 0.5;
}

.node__pip {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
}

.node__pip--locked {
  width: 5px;
  height: 5px;
  background: var(--muted);
}

/* The end of a chapter should look like somewhere you are going. */
.node--boss .node__dot {
  color: var(--pet);
  border-color: var(--pet);
  box-shadow: 0 0 20px rgba(244, 114, 182, 0.28);
}

.node--boss .node__dot--passed {
  background: var(--pet);
  border-color: var(--pet);
  color: var(--bg);
}

.node--boss .node__title {
  color: var(--pet);
}

.node__title,
.node__meta {
  display: block;
  text-align: center;
}

.node__title {
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.35;
}

.node__meta {
  color: var(--muted);
  font-size: 12px;
  line-height: 1.4;
}

.node__link:hover .node__title {
  color: var(--accent);
}

.node--boss .node__link:hover .node__title {
  color: var(--pet);
}

/* Hints ------------------------------------------------------------------ */

.hints {
  margin: 0;
  padding: 0;
  list-style: none;
}

.hint-rung {
  display: flex;
  gap: 12px;
  padding: 14px 20px;
  border-bottom: 1px solid #22262f;
}

.hint-rung--solution {
  background: rgba(244, 114, 182, 0.07);
}

.hint-rung__index {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  margin-top: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--line);
  color: var(--muted-on-fill);
  font-size: 11px;
  font-weight: 600;
}

.hint-rung__body {
  flex: 1 1 auto;
  min-width: 0;
}

.hint-rung__body .prose p {
  margin: 0 0 4px;
  font-size: 14px;
}

.hint-rung__cost {
  color: var(--muted);
  font-size: 12px;
}

.hints__next {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 20px;
  border-bottom: 1px solid #22262f;
}

.hints__next-text {
  flex: 1 1 220px;
  min-width: 0;
}

.hints__next-title {
  margin: 0;
  font-size: 14px;
  color: var(--text-dim);
}

.hints__warning {
  margin: 4px 0 0;
  color: var(--pet);
  font-size: 12px;
  line-height: 1.5;
}

.panel__meta--cost {
  color: var(--pet);
  font-weight: 500;
}

/* Small pieces ----------------------------------------------------------- */

.badge--done {
  background: var(--accent);
  color: var(--bg);
  font-weight: 600;
}

.back {
  color: var(--muted);
  text-decoration: none;
}

.back:hover {
  color: var(--accent);
}

.onward {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 16px 20px;
  border-top: 1px solid var(--line);
  background: var(--surface-raised);
}

.onward__title {
  color: var(--muted);
  font-size: 13px;
}

/* Fenced code inside a brief or a hint ----------------------------------- */

.prose pre {
  margin: 0 0 14px;
  padding: 14px 16px;
  background: var(--editor);
  border: 1px solid var(--line);
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.7;
  /* Scroll rather than clip. Wrapping would be worse: a wrapped line of
     tab-separated fields reads as more fields than there are. */
  overflow-x: auto;
}

.prose pre code {
  padding: 0;
  background: none;
  color: var(--text-dim);
  font-family: inherit;
  font-size: inherit;
}

/* Expected against observed ---------------------------------------------- */

.compare {
  margin: 10px 0 0;
}

.compare__label {
  display: block;
  margin: 0 0 4px;
  color: var(--accent);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.compare__label--got {
  margin-top: 10px;
  color: var(--danger);
}

.compare__text {
  margin: 0;
  padding: 10px 12px;
  background: var(--editor);
  border: 1px solid var(--line);
  border-left: 2px solid var(--accent);
  border-radius: 8px;
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.7;
  overflow-x: auto;
}

.compare__text--got {
  border-left-color: var(--danger);
}

.compare__legend {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: 12px;
}

/* Answered checks -------------------------------------------------------- */

.questions {
  margin: 0;
  padding: 0;
  list-style: none;
}

.question {
  padding: 18px 20px;
  border-bottom: 1px solid #22262f;
}

.question:last-child {
  border-bottom: 0;
}

.question__prompt p:last-child {
  margin-bottom: 12px;
}

.question__options {
  margin: 0;
  padding: 0;
  border: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--editor);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease);
}

.option:hover {
  border-color: var(--line-strong);
}

.option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.option input {
  accent-color: var(--accent);
  width: 16px;
  height: 16px;
  margin: 0;
}

.option__text {
  color: var(--text);
  font-size: 14px;
}

.question__input {
  display: block;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--editor);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 13.5px;
  line-height: 1.7;
  resize: vertical;
}

.question__input:focus {
  outline: none;
  border-color: var(--accent);
}

.toolbar--alone {
  padding: 16px 20px;
}

/* Streak ----------------------------------------------------------------- */

.streak {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
}

.streak:hover {
  border-color: var(--line-strong);
}

/* A streak that has not started yet. Quietened rather than recoloured,
   because the flame's colour is the streak's identity and not a state
   -- and the count beside it already says nought, so the dimming is
   the second cue rather than the only one. */
.streak--cold .icon--game {
  opacity: 0.4;
}

.streak__days {
  font-size: 13px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.streak__label {
  color: var(--muted);
  font-size: 13px;
}

.streak__freezes {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-left: 2px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* Review queue ----------------------------------------------------------- */

.concepts {
  margin: 0;
  padding: 0;
  list-style: none;
}

.concept-due {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
  border-bottom: 1px solid #22262f;
  font-size: 13.5px;
}

.concept-due:last-child {
  border-bottom: 0;
}

.boxes {
  display: flex;
  gap: 3px;
  flex: 0 0 auto;
}

.boxes__step {
  width: 14px;
  height: 5px;
  border-radius: 2px;
  background: var(--line);
}

.boxes__step--held {
  background: var(--accent);
}

.boxes__step--lapsed {
  background: var(--danger);
}

.review-call {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  border: 1px solid var(--border-accent, var(--accent));
  border-radius: var(--radius-card);
  background: var(--accent-soft);
  color: var(--text);
  text-decoration: none;
}

.review-call__count {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.review-call__hint {
  display: block;
  color: var(--muted);
  font-size: 13px;
}

.placement__group {
  padding: 16px 20px;
  border-bottom: 1px solid #22262f;
}

.placement__group .badges {
  margin: 0;
}

/* Craft ------------------------------------------------------------- */

.craft__meter {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 20px 20px 4px;
}

.craft__xp {
  font-size: 34px;
  font-weight: 600;
  color: var(--accent);
  line-height: 1;
}

.craft__unit {
  color: var(--text);
  font-weight: 500;
}

.craft__from {
  margin-left: auto;
  color: var(--muted);
  font-size: 13px;
}

.craft__scan {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 20px 20px;
}

.craft__working {
  color: var(--muted);
  font-size: 13px;
}

/* htmx shows this only while the request is in flight. A scan runs a
   whole test suite, so without it the page looks hung. */
.htmx-indicator {
  display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  display: inline;
}

.craft__outcome {
  margin: 0;
  padding: 0 20px 12px;
  color: var(--text);
  font-size: 14px;
}

.craft__note {
  margin: 0;
  padding: 16px 20px;
  color: var(--muted);
  font-size: 14px;
  line-height: 1.6;
}

.craft__awards {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

.craft__award {
  padding: 14px 20px;
  border-bottom: 1px solid #22262f;
}

.craft__award:last-child {
  border-bottom: 0;
}

.craft__award-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.craft__evidence {
  margin: 4px 0 2px;
  color: var(--muted);
  font-size: 14px;
}

.craft__earned {
  color: var(--muted);
  font-size: 12px;
}

.topbar__link {
  color: var(--muted);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
}

.topbar__link:hover {
  color: var(--text);
}

.craft__ladder {
  padding: 0 20px 16px;
}

.craft__ladder-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 6px;
}

.craft__rank {
  color: var(--text);
  font-size: 15px;
}

.craft__next {
  color: var(--muted);
  font-size: 13px;
}

.craft__bar {
  height: 6px;
  border-radius: 999px;
  background: #22262f;
  overflow: hidden;
}

.craft__bar-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  transition: width 160ms ease-out;
}

/* "Something here wasn't clear" ------------------------------------- */

.unclear {
  margin-top: 14px;
}

.unclear__open {
  cursor: pointer;
  color: var(--muted);
  font-size: 13px;
  list-style: none;
}

.unclear__open::-webkit-details-marker {
  display: none;
}

.unclear__open:hover {
  color: var(--text);
}

.unclear__said {
  display: block;
  width: 100%;
  margin: 10px 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  resize: vertical;
}

.unclear__said:focus {
  outline: none;
  border-color: var(--accent);
}

.unclear__thanks {
  margin: 0;
  color: var(--accent);
  font-size: 13px;
}

.pet__face {
  display: block;
  width: 40px;
  height: 40px;
  object-fit: contain;
}

/* A lapsed streak dims him rather than making him sad. The charter
   rules out punishing mechanics, and a hurt-looking dog because
   somebody missed a day is exactly one. */
.pet--asleep .pet__face {
  filter: grayscale(0.75) brightness(0.6);
}

/* Said to a screen reader, not shown. Used where the visual design
   carries the meaning by position or shape and the words would only be
   noise on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A boss win ------------------------------------------------------------ */

.celebrate {
  margin: 0 0 18px;
  text-align: center;
}

.celebrate__film,
.celebrate__still {
  display: block;
  width: 168px;
  height: 168px;
  margin: 0 auto;
  border-radius: 18px;
  border: 2px solid var(--accent);
  box-shadow: 0 0 30px rgba(163, 230, 53, 0.32);
  object-fit: cover;
}

.celebrate__still {
  display: none;
}

/* Someone who has asked for less movement gets the peak of the jump
   held still instead. The clip is silent and has no audio track at
   all, so there is nothing to mute and nothing to opt into.

   The browser still fetches the video here even when it is hidden,
   because autoplay asks it to. Ninety kilobytes, twice in a whole
   curriculum, is a price worth paying over wiring JavaScript into a
   panel that arrives by swap. */
@media (prefers-reduced-motion: reduce) {
  .celebrate__film {
    display: none;
  }

  .celebrate__still {
    display: block;
  }
}

.celebrate__caption {
  margin: 12px 0 0;
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* Projects --------------------------------------------------------------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.step {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 4px;
  border-bottom: 1px solid #22262f;
}

.step:last-child {
  border-bottom: 0;
}

.step__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px dashed var(--line-strong);
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.step--ready .step__mark {
  border: 2px solid var(--accent);
  border-style: solid;
  color: var(--accent);
}

.step--done .step__mark {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

.step__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  color: inherit;
  text-decoration: none;
}

.step__title {
  font-size: 14.5px;
  font-weight: 500;
}

.step__meta {
  color: var(--muted);
  font-size: 12.5px;
}

/* A step that is waiting is dimmed, not hidden: seeing what is coming
   is the point of a project, and knowing which concept it waits on
   turns "not yet" into an answer. */
.step:not(.step--ready):not(.step--done) .step__title {
  color: var(--muted-on-fill);
}

a.step__body:hover .step__title {
  color: var(--accent);
}

/* A project step just within reach ---------------------------------------- */

.invite {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 16px 20px 0;
  padding: 14px 16px;
  border: 1px solid var(--pet);
  border-radius: var(--radius-card);
  background: rgba(244, 114, 182, 0.06);
  color: inherit;
  text-decoration: none;
}

.invite:hover {
  background: rgba(244, 114, 182, 0.12);
}

.invite__words {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.invite__eyebrow {
  color: var(--pet);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.invite__title {
  font-size: 15px;
}

.invite__meta {
  color: var(--muted);
  font-size: 12.5px;
}

.invite__go {
  display: flex;
  margin-left: auto;
  color: var(--pet);
}

/* The arena ------------------------------------------------------------- */

.stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.arena {
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: flex-start;
}

.board {
  /* Paired with PACE in static/arena.js: a move should land exactly as
     the next one is announced. Change both or neither. */
  --pace: 170ms;

  position: relative;
  width: 100%;
  max-width: 420px;
  aspect-ratio: 1;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  background: var(--editor);
  overflow: hidden;
}

.board__grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  grid-template-rows: repeat(var(--rows), 1fr);
  height: 100%;
}

.board__cell {
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  opacity: 0.55;
}

.board__cell--wall {
  background: var(--line-strong);
  opacity: 1;
}

.board__piece {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(100% / var(--cols));
  height: calc(100% / var(--rows));
  display: grid;
  place-items: center;
  transform: translate(calc(var(--x) * 100%), calc(var(--y) * 100%));
}

.board__piece--tyler {
  transition: transform var(--pace) var(--ease);
  z-index: 2;
}

/* The mascot artwork is a portrait cropped at the collar - a face for
   a 40px avatar, not a sprite that walks. Set in a round token it
   reads as a game piece carrying his face, which is what it is.
   Dropped square on a board it reads as a clipped sprite. Swap this
   for a real walking sprite when there is one. */
.board__piece--tyler img {
  width: 86%;
  height: 86%;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 32%;
  background: var(--surface);
  box-shadow:
    0 0 0 2px var(--accent-soft),
    0 2px 8px rgba(0, 0, 0, 0.55);
  transition: transform var(--dur) var(--ease);
}

/* He faces right in the artwork, so going left is the only direction
   that needs flipping. */
.board__piece--tyler img.is-mirrored {
  transform: scaleX(-1);
}

.board__piece--tyler.is-stuck img {
  filter: drop-shadow(0 0 6px var(--danger));
}

.board__ball {
  width: 54%;
  height: 54%;
  filter: drop-shadow(0 0 8px var(--accent-glow));
}

.board__status {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin: 0;
  min-height: 1.6em;
  color: var(--text-dim);
  font-size: 14px;
}

.board__move {
  color: var(--muted);
}

.board__bump {
  color: var(--danger);
}

.board__verdict {
  color: var(--text);
  font-weight: 600;
}

.board__verdict.is-won {
  color: var(--accent);
}

.board__controls {
  display: flex;
  gap: 8px;
}

/* A line of code quoted inside prose, rather than a whole file shown
   beside an editor: no 240px of empty editor under one signature. */
.code-readonly--inline {
  min-height: 0;
  border-radius: var(--radius-control);
  border: 1px solid var(--line);
  padding: 12px 16px;
}

.panel__note {
  margin: 0;
  padding: 12px 18px 14px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 14px;
}

@media (prefers-reduced-motion: reduce) {
  .board__piece--tyler,
  .board__piece--tyler img {
    transition: none;
  }
}

/* Taking a finished project away -------------------------------------- */

.takeaway {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 12px;
  padding: 14px 18px;
  border: 1px solid var(--accent-soft);
  border-radius: var(--radius-control);
  background: var(--accent-soft);
  color: var(--text);
  text-decoration: none;
  transition: border-color var(--dur) var(--ease);
}

.takeaway:hover {
  border-color: var(--accent);
}

.takeaway strong {
  display: block;
  color: var(--accent);
}

.takeaway__note {
  color: var(--text-dim);
  font-size: 13.5px;
}

/* The shelf: a project that is finished and can be shown to someone -- */

.shelf {
  margin: 0 0 14px;
  padding: 16px 18px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface-raised);
}

.shelf__made {
  margin: 0 0 10px;
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.shelf .concept-row {
  margin-bottom: 12px;
}

.shelf .takeaway {
  margin-bottom: 10px;
}

.shelf__note {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: 13.5px;
}

/* The wardrobe ---------------------------------------------------------- */

/* The sprite and whatever is over it share one box, so the overlay's
   coordinates mean the same thing wherever the pet is drawn. */
.pet__token {
  position: relative;
  display: inline-flex;
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
}

.pet__worn {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.lede--tight {
  margin: 0 0 16px;
  font-size: 14px;
}

.wardrobe {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
}

.wearable {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface-raised);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition:
    border-color var(--dur) var(--ease),
    background var(--dur) var(--ease);
}

.wearable:hover:not(:disabled) {
  border-color: var(--line-strong);
}

.wearable--on {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.wearable--locked {
  cursor: default;
  opacity: 0.55;
}

/* Positioned, because the overlay inside it is absolute: without this
   it escapes to the nearest positioned ancestor and fills the page. */
.wearable__art {
  position: relative;
  display: block;
  width: 40px;
  height: 40px;
  margin-bottom: 6px;
  color: var(--muted);
}

.wearable__art svg,
.wearable__art img {
  width: 100%;
  height: 100%;
}

.wearable__art img {
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 32%;
}

.wearable__name {
  font-weight: 600;
  font-size: 14px;
}

.wearable__note {
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.45;
}

.wearable__note--locked {
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 11.5px;
}

/* What learners flagged ------------------------------------------------- */

.said {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.said__one {
  padding: 12px 16px;
  border-left: 2px solid var(--line-strong);
  background: var(--surface-raised);
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
}

.said__who {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0 0 4px;
  font-size: 13px;
  color: var(--accent);
}

.said__when {
  color: var(--muted);
  font-size: 12px;
}

.said__words {
  margin: 0;
  color: var(--text-dim);
  line-height: 1.6;
}

.said__words--quiet {
  color: var(--muted);
  font-style: italic;
}

.said__go {
  color: var(--text-dim);
  font-size: 14px;
  text-decoration: none;
}

.said__go:hover {
  color: var(--accent);
}

/* The backup line on the feedback page. Quiet when it is fine and
   loud when it is not: the owner should be able to tell at a glance
   without reading the sentence, because the sentence only gets read
   on the days it does not matter. */
.backup {
  display: flex;
  gap: 10px;
  align-items: baseline;
  margin: 0 0 24px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface-raised);
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.6;
}

.backup__mark {
  flex: none;
  font-family: var(--font-mono);
  font-weight: 600;
}

.backup--quiet .backup__mark {
  color: var(--accent);
}

.backup--worrying {
  border-color: var(--danger);
  color: var(--text);
}

.backup--worrying .backup__mark {
  color: var(--danger);
}

/* The pages before you are in --------------------------------------------
   Signing up, signing in, verifying, resetting. They share gate.html,
   which does not extend base.html: a visitor with no account has no
   experience, no streak and no dog, so there is no top bar to draw. */

.gate {
  display: flex;
  min-height: 100vh;
}

.gate__shell {
  display: flex;
  flex-direction: column;
  gap: 18px;
  width: 100%;
  max-width: 420px;
  margin: auto;
  padding: 32px 24px;
}

.gate__mark {
  align-self: center;
}

.gate__card {
  padding: 28px;
}

.gate__form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
}

.gate__label {
  margin-top: 10px;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 500;
}

.gate__input {
  width: 100%;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--editor);
  color: var(--text);
  font: inherit;
  font-size: 14.5px;
}

.gate__input:focus {
  outline: none;
  border-color: var(--accent);
}

.gate__note {
  margin: 2px 0 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.5;
}

.gate__go {
  justify-content: center;
  margin-top: 20px;
}

/* role="alert", so it is announced rather than merely coloured. The
   border carries the same message as the colour for anybody who cannot
   see the difference between red and dim text. */
.gate__problem {
  margin: 0 0 18px;
  padding: 11px 13px;
  border: 1px solid var(--danger);
  border-radius: var(--radius-control);
  background: rgba(248, 113, 113, 0.1);
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.5;
}

.gate__again {
  margin: 18px 0 0;
}

.gate__aside {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  text-align: center;
}

.gate__aside a {
  color: var(--text-dim);
}

.topbar__out {
  margin: 0;
}

/* A button that has to read as the links beside it, because it is one
   of them: the only reason it is a button is that signing out posts. */
.topbar__link--button {
  border: none;
  background: none;
  font: inherit;
  cursor: pointer;
}

/* The landing page. The one route a stranger reaches first, so it
   carries the identity without any of the chrome a learner has
   earned: no experience, no streak, no wardrobe. */
.landing {
  display: flex;
  min-height: 100vh;
}

.landing__shell {
  display: flex;
  flex-direction: column;
  gap: 34px;
  width: 100%;
  max-width: 760px;
  margin: auto;
  padding: 40px 20px 28px;
}

.landing__mark {
  align-self: center;
}

.landing__hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
}

.landing__dog {
  /* Shown at its own size. The art on disk is 122px wide and scaling
     it up is visibly soft - better small and sharp. */
  width: 122px;
  height: 128px;
}

.landing__title {
  margin: 0;
  font-size: clamp(1.7rem, 5vw, 2.5rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.landing__title em {
  color: var(--accent);
  font-style: normal;
}

.landing__lede {
  max-width: 48ch;
  margin: 0;
  color: var(--text-dim);
  font-size: 1.05rem;
  line-height: 1.55;
}

.landing__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin: 6px 0 0;
}

.landing__go {
  box-shadow: 0 0 18px var(--accent-glow);
}

.landing__free {
  margin: 0;
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.landing__points {
  display: grid;
  gap: 14px;
  grid-template-columns: 1fr;
}

@media (min-width: 720px) {
  .landing__points {
    grid-template-columns: repeat(3, 1fr);
  }
}

.landing__point {
  padding: 20px;
}

.landing__point h2 {
  margin: 0 0 8px;
  font-size: 1rem;
  line-height: 1.3;
  color: var(--text);
}

.landing__point p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.55;
}

.landing__foot {
  color: var(--muted);
  font-size: 0.85rem;
  text-align: center;
}

.landing__foot p {
  margin: 0;
}

.landing__foot a {
  color: var(--text-dim);
}

/* Terms, privacy, and taking your data away ----------------------------- */

/* The gate's card is sized for a sign-in form. These two are documents,
   so the shell widens and the prose gets the heading and list rules the
   lesson briefs never needed - a brief is paragraphs and code, and a
   privacy policy is headings, lists and a table. */
.gate__shell--wide {
  max-width: 760px;
}

.legal__draft {
  margin: 0 0 20px;
  padding: 12px 14px;
  border: 1px solid var(--accent);
  border-radius: 10px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.6;
}

.legal__body h1 {
  margin: 0 0 18px;
}

.legal__body h2 {
  margin: 28px 0 12px;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.legal__body h3 {
  margin: 22px 0 10px;
  font-size: 15px;
  font-weight: 600;
}

.legal__body ul,
.legal__body ol {
  margin: 0 0 14px;
  padding-left: 22px;
  color: var(--text-dim);
}

.legal__body li {
  margin-bottom: 7px;
  line-height: 1.6;
}

.legal__body strong {
  color: var(--text);
}

.legal__body hr {
  margin: 24px 0;
  border: none;
  border-top: 1px solid var(--line);
}

.account__go {
  margin: 20px 0 16px;
}

/* Separated from the download above it rather than tinted red. The
   border says "different kind of thing"; the button says which one. */
.account__danger {
  border-color: var(--accent);
}

.account__delete {
  margin-top: 18px;
  max-width: 340px;
}

/* The one destructive button in the application. Outlined rather than
   filled: a solid red block reads as the primary action on the page,
   and deleting your account is not the primary action anywhere. */
.button--danger {
  border-color: #e4572e;
  color: #e4572e;
}

.button--danger:hover {
  background: #e4572e;
  color: #ffffff;
}
