:root {
  --bg: #0a0b0f;
  --surface: #101319;
  --surface-2: #141822;
  --line: #1d2330;
  --line-soft: #171c26;
  --fg: #e7eaf2;
  --dim: #8c94a8;
  --dimmer: #5c6478;
  --accent: #5aa9ff;
  --accent-dim: #2b6fb8;
  --green: #57d38c;
  --amber: #e5b567;
  --radius: 10px;
  --wrap: 72rem;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

html.term-open body { padding-bottom: 19rem; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 1.5rem; }
.dim { color: var(--dim); }
.quiet { color: var(--dim); }

.skip {
  position: absolute; left: -999px;
}
.skip:focus {
  left: 1rem; top: 1rem; z-index: 50; background: var(--accent);
  color: #04101f; padding: 0.5rem 0.75rem; border-radius: 6px;
}

a { color: var(--accent); }

code, kbd, pre { font-family: var(--mono); }
kbd {
  font-size: 0.7rem; border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: 4px; padding: 0.05rem 0.3rem; color: var(--dim);
}

/* ---------- header ---------- */

.site-head {
  border-bottom: 1px solid var(--line);
  background: rgba(10, 11, 15, 0.85);
  backdrop-filter: blur(8px);
  position: sticky; top: 0; z-index: 20;
}
.head-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; min-height: 4.25rem;
}
.brand { text-decoration: none; display: flex; flex-direction: column; line-height: 1.15; }
.brand-mark { font-family: var(--mono); font-weight: 700; color: var(--fg); letter-spacing: -0.02em; }
.brand-sub { font-size: 0.72rem; color: var(--dimmer); font-family: var(--mono); }
.site-nav { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.nav-link {
  color: var(--dim); text-decoration: none; font-size: 0.9rem;
  padding: 0.25rem 0; border-bottom: 1px solid transparent;
}
.nav-link:hover { color: var(--fg); }
.nav-link.is-active { color: var(--fg); border-bottom-color: var(--accent); }

/* The account corner. Signed out it is two doors: a quiet one to sign in, a brighter
   one to create an account. Signed in it collapses to just the handle. */
.nav-account { white-space: nowrap; }
.nav-create {
  border: 1px solid var(--accent-dim);
  border-radius: 999px;
  padding: 0.15rem 0.7rem;
  color: var(--fg);
}
.nav-create:hover { border-color: var(--accent); }
.nav-account.is-in { color: var(--accent); }

/* ---------- hero ---------- */

/*
 * The hero carries Seb's wallpaper.
 *
 * It is mirrored: in the original the brightest sweep sits left of centre, which
 * is exactly where the headline goes. Flipped, the light moves right and the
 * text sits on the dark void, so the artwork keeps its best part and the copy
 * keeps its contrast.
 *
 * The gradient on top is not decoration. Type over a photograph is legible by
 * luck; type over a gradient of known opacity is legible by construction. The
 * bottom fade hands the eye into the page instead of cutting.
 */
.hero {
  padding: 5rem 0 3.5rem;
  position: relative;
  background-image:
    linear-gradient(90deg, #0a0b0f 0%, rgba(10, 11, 15, 0.94) 32%, rgba(10, 11, 15, 0.62) 60%, rgba(10, 11, 15, 0.28) 100%),
    linear-gradient(180deg, rgba(10, 11, 15, 0.6) 0%, rgba(10, 11, 15, 0) 38%, rgba(10, 11, 15, 0.75) 82%, #0a0b0f 100%),
    url('/art/hero.webp');
  background-size: cover, cover, cover;
  background-position: center, center, right center;
  background-repeat: no-repeat;
}
@media (max-width: 760px) {
  .hero {
    background-image:
      linear-gradient(180deg, rgba(10, 11, 15, 0.72) 0%, rgba(10, 11, 15, 0.55) 45%, rgba(10, 11, 15, 0.85) 78%, #0a0b0f 100%),
      url('/art/hero-small.webp');
    background-position: center, center top;
    background-size: cover, cover;
    padding-top: 3.5rem;
  }
}
.hero::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(60% 60% at 20% 0%, rgba(90, 169, 255, 0.10), transparent 70%);
  pointer-events: none;
}
.eyebrow {
  font-family: var(--mono); font-size: 0.78rem; color: var(--accent);
  letter-spacing: 0.08em; margin: 0 0 1rem;
}
/* NOTE: no text-transform here on purpose.
   The l1 in l1ackers must stay lowercase or the whole joke dies: `l1ACKERS`
   reads as H, `L1ACKERS` reads as L-one. A blanket uppercase also mangled every
   path label (`~/knowledge` became `~/KNOWLEDGE`), so caps are written literally
   where they are wanted instead of being imposed on everything. */
.hero h1 {
  font-size: clamp(2rem, 5.5vw, 3.4rem);
  line-height: 1.1; letter-spacing: -0.03em; margin: 0 0 1.25rem; max-width: 22ch;
}
.lede { font-size: 1.1rem; color: var(--dim); max-width: 62ch; margin: 0 0 2rem; }
.actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }

.btn {
  display: inline-block; font-family: var(--sans); font-size: 0.92rem; font-weight: 600;
  padding: 0.65rem 1.15rem; border-radius: 8px; text-decoration: none; cursor: pointer;
  background: var(--accent); color: #04101f; border: 1px solid var(--accent);
}
.btn:hover { background: #7bbcff; border-color: #7bbcff; }
.btn.ghost { background: transparent; color: var(--fg); border-color: var(--line); }
.btn.ghost:hover { border-color: var(--dim); background: var(--surface); }

/* ---------- sections + cards ---------- */

.section-title {
  font-size: 0.85rem; font-family: var(--mono); text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--dimmer); margin: 0 0 1.25rem;
}
.pillars, .shelf { padding: 0 0 3.5rem; }

.cards {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}
.card {
  display: flex; flex-direction: column; gap: 0.35rem;
  padding: 1.15rem 1.25rem; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--surface);
  text-decoration: none; transition: border-color 120ms ease, transform 120ms ease;
}
.card:hover { border-color: var(--accent-dim); transform: translateY(-1px); }
.card-kind {
  font-family: var(--mono); font-size: 0.66rem; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--dimmer);
}
.card-title { color: var(--fg); font-weight: 650; font-size: 1.02rem; }
.card p { margin: 0; color: var(--dim); font-size: 0.9rem; line-height: 1.5; }
.card-cmd { margin-top: auto; padding-top: 0.5rem; font-family: var(--mono); font-size: 0.75rem; color: var(--dimmer); }

.section-rest { padding: 0 0 4rem; }

.files { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--line-soft); }
.files li {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.6rem 0; border-bottom: 1px solid var(--line-soft);
}
.files a { font-family: var(--mono); font-size: 0.9rem; text-decoration: none; }
.files a:hover { text-decoration: underline; }
.files .dim { font-size: 0.78rem; }

/* ---------- interior pages ---------- */

.page-head { padding: 3.25rem 0 2rem; }
.page-head h1 { font-size: clamp(1.6rem, 3.5vw, 2.3rem); letter-spacing: -0.02em; margin: 0 0 0.75rem; }
.prose { max-width: none; padding: 3.25rem 0 4rem; }
/*
 * The reading measure belongs to the text, not to the container.
 *
 * It was `max-width: 68ch` on .prose, which is the right measure for a paragraph and was
 * also being applied to tables. On a 1152px page that left a table 685px wide, so the URL
 * column got 232px and four of the seven tables scrolled sideways inside their own wrapper.
 * Text keeps the measure. A table is not a paragraph and takes the container.
 */
.prose > * { max-width: 68ch; }
.prose > .table-wrap, .prose > .doc-pre { max-width: none; }
.prose h1 { font-size: clamp(1.6rem, 3.5vw, 2.2rem); letter-spacing: -0.02em; margin: 0 0 1.25rem; }
.prose h2 { font-size: 1.25rem; margin: 2.5rem 0 0.75rem; }
.prose h3 { font-size: 1.05rem; margin: 1.75rem 0 0.5rem; }
.prose p, .prose li { color: #d3d8e4; }
.prose a { text-decoration-thickness: 1px; text-underline-offset: 2px; }
.prose code {
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 4px; padding: 0.1rem 0.35rem; font-size: 0.88em;
}
/* Commands are runnable: they look like a button because they are one. */
.prose code.runnable {
  color: var(--green); border-color: #22452f; background: #0e1a13;
  cursor: pointer;
}
/*
 * nowrap is a property of the button in a sentence, not of the button everywhere. In a table
 * cell the command is often a full clone URL, and nowrap there guarantees it spills across the
 * column: no wrap opportunities exist, so overflow-wrap has nothing to work with. This rule
 * has two classes and beat the cell rules on specificity, which is how it survived two rounds
 * of fixing the table.
 */
.prose p code.runnable, .prose li code.runnable, .prose blockquote code.runnable { white-space: nowrap; }
.prose code.runnable::before { content: "$ "; color: var(--dimmer); }
.prose code.runnable:hover { border-color: var(--green); background: #12241a; }
.prose .code {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1rem 1.15rem; overflow-x: auto; font-size: 0.86rem; line-height: 1.55;
}
.table-wrap { overflow-x: auto; margin: 0 0 1.5rem; }
/*
 * Tables: readable size, honest columns, and no character-by-character collapse.
 *
 * Two mistakes are recorded here, both found by looking at a render rather than at the CSS.
 *
 * 1. `overflow-wrap: anywhere` on every cell makes a cell's minimum width ONE character.
 *    With auto layout the browser then satisfies a long URL by shrinking its neighbours to
 *    their minimum, so a tool name renders as a vertical ribbon of single letters.
 *    `break-word` breaks only when the word genuinely does not fit, and does not touch the
 *    minimum, which is the behaviour wanted here.
 *
 * 2. Auto layout still lets a long unbreakable URL set the floor for the whole table. Fixed
 *    layout plus explicit widths makes the browser obey the columns instead of guessing, and
 *    then the URL breaks inside its own cell where it belongs.
 *
 * Size: 1.05rem, deliberately above the body. Tables here are mostly monospace, and monospace
 * at the same nominal size reads smaller than sans, so measuring equal reads as fine print.
 */
.prose table { border-collapse: collapse; width: 100%; font-size: 1.05rem; table-layout: fixed; }
.prose th, .prose td {
  border: 1px solid var(--line); padding: 0.7rem 0.9rem; text-align: left;
  vertical-align: top; overflow-wrap: break-word;
}
.prose th:first-child, .prose td:first-child { width: 20%; }
.prose th:nth-child(2), .prose td:nth-child(2) { width: 34%; }
.prose td code, .prose th code {
  /*
   * `anywhere` and NOTHING ELSE. `word-break: break-word` is a legacy alias that Chromium
   * maps to `overflow-wrap: break-word`, so declaring it alongside `anywhere` silently
   * overrides it and the URL stops wrapping. Two spellings of "break long words" where the
   * second is an alias of a weaker rule is a trap worth naming.
   */
  font-size: 1em; white-space: normal; overflow-wrap: anywhere;
}
/*
 * `anywhere` here, and deliberately not on the cells.
 *
 * `anywhere` on a cell makes its minimum width one character, and with auto layout the
 * browser then crushes the neighbouring columns to satisfy a long URL: "osquery" rendered as
 * a vertical ribbon of single letters. That is the bug this rule replaced.
 *
 * Inside a cell it is safe, because `table-layout: fixed` has already assigned the widths and
 * removed the min-content feedback entirely. A URL with no spaces then wraps within its
 * column instead of spilling across the table, which is what `break-word` failed to do.
 */
.prose th { color: var(--dim); font-weight: 600; background: var(--surface); }
.doc-pre {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.5rem 1.75rem; overflow-x: auto; line-height: 1.7; color: #d3d8e4;
}

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

.site-foot { border-top: 1px solid var(--line); padding: 2rem 0; margin-top: 2rem; }
.foot-inner {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  font-size: 0.85rem; color: var(--dim);
}
.foot-inner .dot { color: var(--dimmer); }
.term-toggle {
  margin-left: auto; font-family: var(--mono); font-size: 0.8rem;
  background: var(--surface); color: var(--fg); border: 1px solid var(--line);
  border-radius: 7px; padding: 0.4rem 0.7rem; cursor: pointer;
}
.term-toggle:hover { border-color: var(--accent-dim); }
.page-cmd { font-family: var(--mono); font-size: 0.78rem; color: var(--dimmer); }

/* ---------- provenance strip ---------- */

.article-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
  margin: 0 0 1.6rem; padding-bottom: 1rem; border-bottom: 1px solid var(--line);
  font-family: var(--mono); font-size: 0.78rem;
}
.chip {
  border: 1px solid var(--accent-dim); color: var(--accent);
  border-radius: 999px; padding: 0.1rem 0.6rem; letter-spacing: 0.03em;
}
.meta-date { color: var(--dimmer); }
.meta-source { color: var(--dim); text-decoration: none; border-bottom: 1px solid var(--line); }
.meta-source:hover { color: var(--accent); border-bottom-color: var(--accent-dim); }
.meta-nosource { color: #ffb37a; border-bottom-color: #ffb37a; }

/* ---------- voting ---------- */

.card-row { display: flex; gap: 0.6rem; align-items: stretch; }
.card-row .card { flex: 1; }

.vote {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.1rem; padding: 0.5rem 0.45rem; min-width: 3.1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
  font-family: var(--mono);
}
.vote-btn {
  background: none; border: 0; cursor: pointer; padding: 0.1rem 0.2rem;
  color: var(--dimmer); font-size: 0.8rem; line-height: 1; border-radius: 4px;
}
.vote-btn:hover { color: var(--fg); background: var(--surface-2); }
.vote-btn.is-mine[data-value="1"] { color: var(--green); }
.vote-btn.is-mine[data-value="-1"] { color: #ff7a7a; }
.vote-score { font-size: 0.82rem; color: var(--fg); font-variant-numeric: tabular-nums; }
.vote-note { font-size: 0.68rem; color: var(--dimmer); max-width: 5rem; text-align: center; line-height: 1.2; }

.article-vote { display: flex; align-items: center; gap: 0.75rem; padding: 3rem 0 0; }
.article-vote .vote { flex-direction: row; min-width: 0; gap: 0.35rem; padding: 0.35rem 0.6rem; }
.vote-hint { color: var(--dimmer); font-family: var(--mono); font-size: 0.76rem; }

/* ---------- account ---------- */

.wrap.narrow { max-width: 46rem; }
.account-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); padding-bottom: 4rem; }
.stack { display: flex; flex-direction: column; gap: 0.75rem; border: 1px solid var(--line); border-radius: var(--radius); padding: 1.25rem; background: var(--surface); }
.stack h2 { font-size: 0.85rem; font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--dimmer); margin: 0; }
.stack label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.82rem; color: var(--dim); font-family: var(--mono); }
.stack input {
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
  padding: 0.5rem 0.65rem; color: var(--fg); font-family: var(--mono); font-size: 0.9rem;
}
.stack input:focus { outline: none; border-color: var(--accent-dim); }
.stack .btn { align-self: flex-start; }
.form-note { margin: 0; font-size: 0.8rem; color: var(--dim); font-family: var(--mono); min-height: 1.2em; }
.form-note.bad { color: #ff9a9a; }
.form-note.ok { color: #8ce99a; }

/* A statement of fact rather than an alert: the site has no popups, so a page that needs
   to say something says it in the flow. */
.notice { margin: 0 0 1.5rem; padding: 0.75rem 1rem; border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); background: var(--surface); font-size: 0.85rem; color: var(--dim); }
.notice.ok { border-left-color: #8ce99a; }

/* The answer shelf. A reveal is a promise that the fix is behind one honest click,
   so the border is quiet and the summary reads like a control, not a headline.
   Open, the body is the page's own typography inside a lighter surface, which is
   the visual cue that this was always part of the lesson, just held back. */
details.reveal { margin: 1.75rem 0; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--surface); overflow: hidden; }
details.reveal > summary { cursor: pointer; list-style: none;
  padding: 0.7rem 1rem; font-family: var(--mono); font-size: 0.82rem;
  color: var(--dim); user-select: none; }
details.reveal > summary::-webkit-details-marker { display: none; }
details.reveal > summary::before { content: '+ '; }
details.reveal[open] > summary { border-bottom: 1px solid var(--line-soft); color: var(--dimmer); }
details.reveal[open] > summary::before { content: '\2013 '; }
details.reveal > .reveal-body { padding: 1rem 1rem 0.25rem; background: var(--surface-2); }
details.reveal > .reveal-body > :last-child { margin-bottom: 1rem; }


.account-block { margin: 0 0 2rem; }
.account-block h2 { font-size: 0.85rem; font-family: var(--mono); text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--dimmer); margin: 0 0 0.75rem; }
.account-block .stack { margin-top: 1rem; }
.account-block details summary { cursor: pointer; font-family: var(--mono); font-size: 0.82rem; color: var(--dim); }
.account-block details[open] summary { margin-bottom: 1rem; }

/* The flag shelf. Earned flags are the strings themselves, and nothing else is
   shown: no slots, no count of what is left, because the hunt is the point. */
.flag-list { list-style: none; margin: 0 0 0.25rem; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.flag-row code {
  display: inline-block; min-width: 100%; box-sizing: border-box;
  font-family: var(--mono); font-size: 0.85rem; padding: 0.45rem 0.6rem;
  border-radius: 6px; border: 1px solid var(--line);
}
.flag-won { color: var(--fg); background: var(--surface-2); }

/* The lesson bookmark: a click, not a gate. The lesson reads identically either way, and
   signing out only removes the tick, never the page. */
.article-lesson { margin: 0 0 1.6rem; }
.lesson-widget { display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap;
  padding: 0.8rem 1rem; border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); background: var(--surface); }
.lesson-widget .btn { flex: 0 0 auto; }
.lesson-state { font-size: 0.8rem; font-family: var(--mono); color: var(--dim); min-height: 1.2em; }
.lesson-state a { color: var(--accent); }

/* The next lesson, pushed to the far side of the widget and hidden until the mark is set.
   The [hidden] guard matters: a display rule on the element would otherwise defeat it, which
   is how a "hidden" control ends up visible. */
/* The share card, shown on the piece it belongs to so the page and the preview agree. */
.article-card {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: var(--radius);
  margin: 0 0 1.4rem;
}
/* The same card as a thumbnail in a listing. */
.card-thumb {
  display: block; width: 100%; height: auto; aspect-ratio: 1200 / 630; object-fit: cover;
  border: 1px solid var(--line-soft); border-radius: 8px; margin-bottom: 0.7rem;
}

/*
 * The board: a ranked list, so it is read top to bottom rather than scanned as a grid.
 *
 * The rank is a column of its own so the numbers line up down the page, which is the only
 * thing that makes a ranking legible at a glance.
 */
.board { list-style: none; margin: 1.5rem 0 0; padding: 0; }
.board-row { display: flex; gap: 0.85rem; padding: 0.6rem 0; border-bottom: 1px solid var(--line); }
.board-rank {
  min-width: 1.7rem; text-align: right; color: var(--dimmer);
  font-family: var(--mono); font-size: 0.9rem; padding-top: 0.2rem;
}
.board-main { min-width: 0; }
.board-title { font-size: 1.05rem; }
.board-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem;
  margin-top: 0.25rem; font-size: 0.85rem; color: var(--dim);
}
.board-sep { color: var(--dimmer); }
.board-cmd { font-family: var(--mono); font-size: 0.8rem; color: var(--dimmer); }

/*
 * Figures: a page can now show an image, because `![](src)` renders and `cat` shows the same
 * line, so the tree story holds and nothing needs a second content pipeline.
 *
 * Two-up, with slack for the whitespace.
 *
 * Two inline-blocks at half width plus a margin come to exactly 100%, and the newline between
 * them is a real text node about 4px wide, which tips the row over and stacks every figure one
 * per row. The widths are set to leave the gap the whitespace needs.
 *
 * A uniform aspect ratio with `object-fit: cover` makes a row of different mockups read as a
 * set. The crop is visual only, the file is untouched.
 */
.figure { display: inline-block; width: calc(50% - 1.5rem); vertical-align: top; margin: 0 0 1.4rem; }
.figure:nth-of-type(odd) { margin-right: 1.5rem; }
.figure img {
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block;
  border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2);
}
.figure figcaption { margin-top: 0.45rem; font-size: 0.85rem; color: var(--dim); }
.img-refused { color: var(--dimmer); font-style: italic; }

.lesson-next { margin-left: auto; white-space: nowrap; }
.lesson-widget [hidden] { display: none !important; }

/* The same control again at the end of the lesson: after reading, it should be where the
   eyes already are. The first one arrives with the prose; this one follows it. */
.prose + .article-lesson { margin: 2.2rem 0 0; }

/* A notice that is a refusal rather than a confirmation. It has to be loud enough that a
   reader never concludes "nothing happened", which is what a dim line of text reads as. */
.notice.bad { border-left: 3px solid #e0564a; color: #f4bdb6; background: rgba(224, 86, 74, 0.08); }

/* ---------- the docked terminal ---------- */

.terminal {
  /* 24rem rather than 18: an inch of extra room is about six more lines of history, which is
     the difference between watching a four-stage chain work and guessing at it. */
  position: fixed; left: 0; right: 0; bottom: 0; height: 24rem;
  background: rgba(9, 10, 14, 0.97);
  border-top: 1px solid var(--line);
  display: flex; flex-direction: column;
  transform: translateY(100%); transition: transform 180ms ease;
  z-index: 30; backdrop-filter: blur(6px);
}
html.term-open .terminal { transform: translateY(0); }

.term-head {
  display: flex; align-items: center; gap: 1rem;
  padding: 0.5rem 1.25rem; border-bottom: 1px solid var(--line-soft);
  font-family: var(--mono); font-size: 0.76rem;
}
.term-title { color: var(--fg); }
.term-note { color: var(--dimmer); }
.term-close {
  margin-left: auto; background: none; border: 1px solid var(--line);
  color: var(--dim); border-radius: 5px; padding: 0.15rem 0.5rem;
  font-family: var(--mono); font-size: 0.72rem; cursor: pointer;
}
.term-close:hover { color: var(--fg); border-color: var(--dim); }

.term-log { flex: 1; overflow-y: auto; padding: 0.85rem 1.25rem; font-family: var(--mono); font-size: 0.84rem; }
.term-out { margin: 0 0 0.6rem; white-space: pre-wrap; word-break: break-word; color: #c8cede; }
.term-out a { color: var(--accent); text-decoration: none; }
.term-out a:hover { text-decoration: underline; }
.term-echo { color: var(--dimmer); margin-bottom: 0.4rem; font-family: var(--mono); font-size: 0.84rem; }
.term-suggest { color: var(--dimmer); font-family: var(--mono); font-size: 0.82rem; margin: 0.35rem 0 0.8rem; }
.term-suggest code {
  color: var(--green); border: 1px solid #22452f; background: #0e1a13;
  border-radius: 4px; padding: 0.05rem 0.35rem; cursor: pointer;
}
.term-suggest code:hover { border-color: var(--green); background: #12241a; }
.term-echo .prompt { color: var(--green); }

.term-form {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.65rem 1.25rem; border-top: 1px solid var(--line-soft);
  font-family: var(--mono);
}
.term-form .prompt { color: var(--green); font-size: 0.9rem; }
.term-input {
  flex: 1; background: none; border: 0; outline: none;
  color: var(--fg); font-family: var(--mono); font-size: 0.9rem;
}
.term-input::placeholder { color: #3f4657; }

@media (max-width: 640px) {
  .hero { padding: 3rem 0 2rem; }
  .head-inner { flex-direction: column; align-items: flex-start; padding: 0.85rem 0; }
  .site-nav { gap: 0.9rem; }
  .terminal { height: 60vh; }
  html.term-open body { padding-bottom: 62vh; }
}

/* Suggestion chips are real buttons now, because <code> is not focusable and the
   hints were therefore mouse-only. Styled to read exactly as they did before. */
.term-suggest button {
  font: inherit;
  font-family: inherit;
  background: transparent;
  border: 0;
  padding: 0 0.15em;
  margin: 0;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.term-suggest button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

/* Course tiers on the tutorials index. Round because a stage is a stage, not a status. */
.card-tier {
  display: inline-block;
  align-self: flex-start;
  padding: 0.1em 0.7em;
  border: 1px solid rgba(63, 185, 80, 0.55);
  border-radius: 999px;
  background: rgba(63, 185, 80, 0.12);
  color: #56d364;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* One colour per tier, so the stages are separable at a glance down a long index.
   Green is the ground floor, Field Work is one step up and reads as amber, Live Fire
   is the tier that leaves the jail and reads as red on the dark palette.
   A tier with no rule of its own falls back to the neutral badge above. */
.card-tier.tier-first-steps {
  border-color: rgba(63, 185, 80, 0.55);
  background: rgba(63, 185, 80, 0.12);
  color: #56d364;
}
.card-tier.tier-field-work {
  border-color: rgba(227, 179, 65, 0.6);
  background: rgba(227, 179, 65, 0.13);
  color: #e3b341;
}
.card-tier.tier-live-fire {
  border-color: rgba(248, 81, 73, 0.6);
  background: rgba(248, 81, 73, 0.12);
  color: #f85149;
}

/* The stage break on a long index: a small label and a hairline that fades out, nothing more. */
.tier-break {
  grid-column: 1 / -1;
  flex-basis: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin: 1.5rem 0 0.45rem;
  color: #7d8698;
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.tier-break::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(to right, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0));
}

/* ---------- full screen ---------- */

/* The shell stops being a dock and becomes the room. 100dvh rather than 100vh: the mobile
   viewport is dynamic, and 100vh lies whenever the browser chrome shows or hides. */
html.term-full .terminal {
  height: 100dvh;
}

/*
 * The URL weld holds in here, and it holds by one rule: full screen hides the page, never the
 * position. The shell's cwd is read from window.location.pathname on every command, and `cd`
 * and `open` move by assigning that URL, so a reader in full screen who walks to 021 is
 * standing in 021 and the page is simply behind the shell. Exit and you are there.
 *
 * The alternative rule was defensible: full screen locks where you stood and the URL stops
 * moving you. Both are coherent. Mixed is not, and the mixture is what happens by accident
 * when a display:none is added without deciding. This is the decision, and it is the same one
 * the docked shell already made, because it is the same tree.
 */
html.term-full .page { display: none; }

.term-full {
  margin-left: auto; font-family: var(--mono); font-size: 0.76rem;
  background: none; color: var(--dimmer); border: 1px solid transparent;
  border-radius: 6px; padding: 0.2rem 0.6rem; cursor: pointer;
}
.term-full:hover { color: var(--fg); border-color: var(--line); }
.term-close { margin-left: 0.4rem; }
