* { box-sizing: border-box; }

/*
 * The UA stylesheet's [hidden] rule is a plain `display: none` and loses to ANY author
 * `display` declaration. That has silently broken four elements across two phases — tabs,
 * editor panes, and this file field — each time discovered by a person rather than a test.
 * One !important retires the whole family.
 */
[hidden] { display: none !important; }

/*
 * A column flex layout so the footer is pinned to the bottom of the viewport on short pages —
 * a 404 or a one-paragraph lesson used to leave it floating halfway up the screen, which reads
 * as the page having failed to load the rest of itself.
 *
 * dvh after vh: on mobile 100vh is the viewport with the browser chrome RETRACTED, so a vh-only
 * rule leaves the footer pushed below the fold behind the address bar. dvh tracks the chrome as
 * it hides and shows; the vh line stays as the fallback for anything that lacks dvh.
 */
body {
    margin: 0;
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    background: var(--bg);
    color: var(--text);
    font: 16px/1.7 var(--sans);
}

/* Takes the slack so the footer sits below it. `1 0 auto`, not `1`: a 0% basis would let a
   long page's content drive its own height from zero and fight the max-width centring. */
.site-main { flex: 1 0 auto; }
.site-footer { margin-top: auto; }

/*
 * Display sizes are set tight because Inter's default tracking is tuned for text, not for
 * 2.6rem headlines — left alone it reads loose and corporate at this scale. The negative
 * tracking is what makes a heavy weight look drawn rather than merely bolded.
 */
h1 { font-family: var(--display); font-weight: 800; letter-spacing: -.03em; line-height: 1.12; font-size: 2.4rem; margin: 0 0 var(--space-4); }
h2 { font-family: var(--display); font-weight: 700; letter-spacing: -.02em; line-height: 1.2; }
h3 { font-family: var(--display); font-weight: 700; letter-spacing: -.015em; }

/* Arabic never takes the Latin tracking — negative letter-spacing breaks Arabic joining. */
h1 .ar, h2 .ar, h3 .ar, [lang="ar"] { letter-spacing: normal; }

/*
 * .ar is sized for body prose (.95rem). Nested in a heading that leaves a 38px English title
 * followed by a 15px Arabic fragment — the pair stops reading as one title and wraps
 * mid-phrase. em rather than rem so the Arabic stays proportional at every heading size,
 * including the clamp() sizes later tasks introduce.
 *
 * Deliberately scoped to Arabic INSIDE a heading. The bare [lang="ar"] on line 28 is right
 * for letter-spacing, but must not carry font-size — that would shrink every Arabic
 * paragraph on the site. An <h1 lang="ar"> whose whole content is Arabic (views/admin/detail.php:4)
 * is also untouched on purpose: it is the heading, not a translation beside one.
 */
h1 .ar, h2 .ar, h3 .ar,
h1 [lang="ar"], h2 [lang="ar"], h3 [lang="ar"] {
    font-size: .82em;
    font-weight: inherit;
}

/*
 * The .82em rule above is for display headings, where a full-size Arabic span would tower
 * over tight Latin. A label bar is already .72rem chrome, so compounding drops its Arabic to
 * ~9px. In a label both languages sit at the same size.
 */
.island-label [lang="ar"], .island-label .ar { font-size: 1em; }

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

/*
 * Ligatures OFF wherever code is shown.
 *
 * JetBrains Mono draws == as one double-bar glyph and === as one triple-bar glyph. That is
 * a pleasant thing in an editor, where you already know what you typed, and a defect in a
 * lesson: php/comparison exists to teach the difference between those two operators, and
 * with ligatures on a student cannot see that one is two characters and the other is three.
 * The same applies to => -> != >= <= throughout every course.
 *
 * Verified in Chrome against that lesson — the operators were genuinely indistinguishable.
 */
code, pre, kbd, samp {
    font-family: var(--mono);
    font-variant-ligatures: none;
    font-feature-settings: "liga" 0, "calt" 0;
}

/* ---------- chrome ---------- */

/*
 * Deliberately NOT sticky — it scrolls away with the page.
 *
 * The shadow is therefore constant rather than applied on scroll: a scroll-triggered lift only
 * says something when content is passing underneath, and here nothing ever does. A steady, very
 * soft shadow just separates the chrome from the page beneath it.
 */
.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: .7rem 1.5rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    box-shadow: var(--shadow-1);
}

/* ---------- brand lockup ---------- */

.brand { gap: var(--space-3); }

/*
 * The mark repeats the favicon so a tab and the page agree. Filled with --accent rather than
 * --brand: --accent-text is designed and measured to sit on --accent in both themes, and the
 * brand blue has no such guaranteed partner — white on the dark theme's #5c9fff is 2.3:1.
 */
.brand-mark {
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    flex: none;
    border-radius: var(--radius);
    background: var(--accent);
    color: var(--accent-text);
    font-family: var(--brand-font);
    font-weight: 600;
    font-size: 1.2rem;
    line-height: 1;
}

.brand-word { line-height: 1; }

/* ---------- courses menu ---------- */

.nav-menu { position: relative; display: flex; }

.nav-trigger {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    min-height: 44px;
    padding: 0 .7rem;
    background: transparent;
    border: 0;
    border-radius: var(--radius);
    color: var(--muted);
    font: inherit;
    font-size: .92rem;
    font-weight: 500;
    white-space: nowrap;
    cursor: pointer;
    transition: color .12s, background-color .12s;
}

.nav-trigger:hover { color: var(--text); background: var(--surface-2); }
.nav-trigger[aria-expanded="true"] { color: var(--text); background: var(--surface-2); }
.nav-trigger[aria-expanded="true"] .icon-chevron-down { transform: rotate(180deg); }
.nav-trigger .icon { transition: transform .18s; }

/*
 * Anchored to the trigger and scrollable, so the menu is the one part of the header that does
 * not care how many courses exist — twenty entries scroll rather than pushing the panel past
 * the bottom of the screen.
 */
.nav-panel {
    position: absolute;
    top: calc(100% + .4rem);
    right: 0;
    z-index: 30;
    min-width: 17rem;
    max-height: min(60vh, 26rem);
    overflow-y: auto;
    padding: var(--space-2);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
    box-shadow: var(--shadow-3);
}

.nav-panel a {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: .55rem .6rem;
    border-radius: var(--radius);
    text-decoration: none;
    color: var(--text);
    transition: background-color .12s;
}

.nav-panel a:hover { background: var(--surface-2); }
.nav-text { display: grid; gap: .1rem; min-width: 0; }
.nav-text strong { font-size: .92rem; font-weight: 600; }
.nav-text small { color: var(--muted); font-size: .78rem; font-family: var(--mono); }

/* The course's own gradient, so the menu identifies a course the same way its tile does. */
.nav-dot {
    width: .55rem;
    height: .55rem;
    flex: none;
    margin-top: .45rem;
    border-radius: var(--radius-pill);
    background: var(--course-grad);
}

.nav-link {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 .7rem;
    border-radius: var(--radius);
    text-decoration: none;
    color: var(--muted);
    font-size: .92rem;
    font-weight: 500;
    white-space: nowrap;
    transition: color .12s, background-color .12s;
}

.nav-link:hover { color: var(--text); background: var(--surface-2); }

.brand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    font-family: var(--brand-font);
    /*
     * Demi rather than bold: at wordmark size the heavier weight closes up the counters in
     * the d and the a, and the mark stops reading as round.
     */
    font-weight: 600;
    font-size: 1.35rem;
    /*
     * A wordmark is set tighter than running text — the letters belong to one shape rather
     * than to a sentence. Past about -.03em the u and the a start to touch.
     */
    letter-spacing: -.02em;
    text-decoration: none;
    color: var(--brand);
}

.site-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 1.25rem;
}

.site-nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 .55rem;
    text-decoration: none;
    color: var(--muted);
    font-size: .92rem;
    font-weight: 500;
    /*
     * .site-header's flex-wrap wraps the header's OWN children (brand vs nav) — it does
     * nothing for the links inside .site-nav, which needs its own flex-wrap above. With that
     * wrap available, this keeps each bilingual label whole and lets whole links move to the
     * next row instead of breaking mid-label.
     */
    white-space: nowrap;
    transition: color .12s;
}

.site-nav a:hover { color: var(--text); }

/*
 * width:100% is load-bearing, not belt-and-braces. body is a column flex container (so the
 * footer can be pinned), and a flex item with an AUTO cross-axis margin does not stretch —
 * the auto margins absorb the free space and the item shrink-to-fits its content instead.
 * With margin:0 auto and no width, this box was sized by whatever happened to be inside it:
 * pages of prose hit the max-width and looked correct, while the playground collapsed to
 * 910px inside a 1500px allowance. An explicit width restores the stretch; max-width still
 * caps it and the auto margins still centre it.
 */
.site-main { width: 100%; max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }

.site-footer {
    border-top: 1px solid var(--border);
    background: var(--surface);
    color: var(--muted);
    padding: var(--space-6) 1.5rem;
    font-size: .9rem;
}

.footer-inner {
    max-width: 1100px;
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-6);
}

.footer-brand { display: flex; align-items: flex-start; gap: var(--space-3); max-width: 34rem; }
.footer-name { margin: 0; font-family: var(--brand-font); font-weight: 600; font-size: 1.05rem; color: var(--text); }
.footer-tagline { margin: .2rem 0 0; line-height: 1.6; }

.footer-nav { display: grid; gap: .1rem; align-content: start; }


.footer-nav a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 34px;
    color: var(--muted);
    text-decoration: none;
}

.footer-nav a:hover { color: var(--text); }

/* ---------- building blocks ---------- */

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
    padding: 1.5rem;
}

/*
 * The island. One primitive, used for every discrete block on the site.
 *
 * Separation is done by a hard border plus a background step, never by shadow — shadow is
 * reserved for things that genuinely float (the login card) and for a lift on islands you can
 * actually click. A soft shadow on every block is what made the old site read as generic.
 */
.island {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
}

/*
 * The label sits in a title bar across the island's top edge, the way an IDE tool window
 * names itself. This is what turns EDITOR, QUIZ and TASK from three form sections into three
 * instruments — the reader can tell at a glance which parts of the page are things to use.
 */
.island-label {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    padding: .55rem var(--space-4);
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
    font-family: var(--mono);
    font-size: .72rem;
    font-weight: 400;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}

.island-body { padding: var(--space-5); }

/* Only for islands that are links. The border does the identifying; the lift confirms it. */
.island.is-lift { transition: border-color .15s, box-shadow .15s; }
.island.is-lift:hover { border-color: var(--border-strong); box-shadow: var(--shadow-2); }

/*
 * The mono eyebrow: a small, tracked, uppercase line above a heading that says what kind of
 * thing follows. Mono is doing chrome work here rather than code work, which is the single
 * most JetBrains move in the system.
 */
.eyebrow {
    font-family: var(--mono);
    font-size: .74rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 var(--space-2);
}

/*
 * Course identity, scoped by an attribute that already exists on .lesson, .progress and
 * .editor — so the gradient reaches them with no new markup. Task 4 adds the same attribute
 * to the home course card, which is the only place it was missing.
 *
 * The fallback on :root matters: a course added to content/ with no rule here still gets a
 * coherent accent instead of an invalid var() and an unstyled bar. That fallback is also why
 * the omission is invisible — the terminal course was added with its colours defined in
 * tokens.css and no rule here, and every page simply used the generic accent while looking
 * perfectly fine. A test now derives this list from the registry so it cannot happen again.
 */
:root { --course-grad: var(--accent); --course-solid: var(--accent); }
[data-course="web"]      { --course-grad: var(--course-web-grad);      --course-solid: var(--course-web); }
[data-course="terminal"] { --course-grad: var(--course-terminal-grad); --course-solid: var(--course-terminal); }
[data-course="git"]      { --course-grad: var(--course-git-grad);      --course-solid: var(--course-git); }
[data-course="framework"] { --course-grad: var(--course-framework-grad);  --course-solid: var(--course-framework); }
[data-course="basics"]    { --course-grad: var(--course-basics-grad);     --course-solid: var(--course-basics); }
[data-course="javascript"]{ --course-grad: var(--course-javascript-grad); --course-solid: var(--course-javascript); }
[data-course="html"]     { --course-grad: var(--course-html-grad);     --course-solid: var(--course-html); }
[data-course="css"]      { --course-grad: var(--course-css-grad);      --course-solid: var(--course-css); }
[data-course="engineering"]{ --course-grad: var(--course-engineering-grad); --course-solid: var(--course-engineering); }
[data-course="testing"]   { --course-grad: var(--course-testing-grad);    --course-solid: var(--course-testing); }
[data-course="automation"]{ --course-grad: var(--course-automation-grad); --course-solid: var(--course-automation); }
[data-course="design"]    { --course-grad: var(--course-design-grad);     --course-solid: var(--course-design); }
[data-course="systems"]   { --course-grad: var(--course-systems-grad);    --course-solid: var(--course-systems); }
[data-course="php"]       { --course-grad: var(--course-php-grad);        --course-solid: var(--course-php); }
[data-course="production"]{ --course-grad: var(--course-production-grad); --course-solid: var(--course-production); }
[data-course="databases"] { --course-grad: var(--course-databases-grad);  --course-solid: var(--course-databases); }
[data-course="working"]   { --course-grad: var(--course-working-grad);    --course-solid: var(--course-working); }

.narrow { max-width: 640px; margin: 3rem auto; text-align: center; }

.error-island { border-radius: var(--radius); }

/* The status code as the display element. Mono at this size stops being code and becomes a
   number you are meant to read as a label — which is exactly what an HTTP status is. */
.error-code {
    font-family: var(--mono);
    font-size: clamp(3.5rem, 12vw, 6rem);
    font-weight: 700;
    line-height: 1;
    letter-spacing: -.04em;
    color: var(--course-solid);
    margin: 0 0 var(--space-3);
}

.btn {
    display: inline-block;
    background: var(--accent);
    color: var(--accent-text);
    border: 0;
    border-radius: var(--radius);
    padding: .6rem 1.1rem;
    font: inherit;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}

.btn:hover { filter: brightness(1.1); }
.btn.ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn:disabled { opacity: .5; cursor: not-allowed; }

.ar { direction: rtl; text-align: right; font-size: .95rem; }

/* ---------- home ---------- */

.hero { margin-bottom: var(--space-6); }
.hero .island-body { padding: var(--space-7) var(--space-6); }
.hero h1 { font-size: clamp(2.2rem, 5vw, 3.4rem); max-width: 20ch; }
.hero .lead { font-size: 1.15rem; max-width: 60ch; }

.course-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }

.course-card {
    display: block;
    text-decoration: none;
    color: inherit;
    /* The gradient stripe is the first thing painted, so the course is identified before a
       single word is read. overflow:hidden keeps it inside the island's 2px corners. */
    overflow: hidden;
    position: relative;
}

.course-card::before {
    content: '';
    display: block;
    height: 4px;
    background: var(--course-grad);
}

.course-card h2 { margin: 0 0 var(--space-1); font-size: 1.4rem; }
.course-card .tagline { color: var(--muted); margin: 0 0 var(--space-3); }
.course-card .count { color: var(--course-solid); font-weight: 600; font-size: .9rem; font-family: var(--mono); }

/* ---------- course page ---------- */

.progress-bar { height: 6px; background: var(--surface-2); border-radius: var(--radius-pill); overflow: hidden; margin: 0 0 var(--space-3); }
/* The bar carries the same gradient as the course stripe above it — one course, one mark. */
.progress-bar > span { display: block; height: 100%; background: var(--course-grad); width: 0; transition: width .3s; }
.progress-text { margin: 0; font-family: var(--mono); font-size: .8rem; }

/*
 * A rail, not a stack of cards. Thirty lessons boxed individually is thirty borders competing
 * with each other; one bounding border with hairline rules between rows lets the eye run down
 * the numbers, which is how a curriculum is actually scanned.
 */
.lesson-rail {
    list-style: none;
    margin: var(--space-5) 0 0;
    padding: 0;
    display: block;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
    overflow: hidden;
}

.lesson-rail li + li { border-top: 1px solid var(--border); }

.lesson-rail a {
    display: flex;
    align-items: baseline;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
    background: transparent;
    border: 0;
    border-radius: 0;
    text-decoration: none;
    color: inherit;
    transition: background-color .12s;
}

.lesson-rail a:hover { background: var(--surface-2); }

/* Tabular mono numerals in a fixed gutter, so 1 and 30 occupy the same column. */
.lesson-rail .num {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    min-width: 2.5ch;
    text-align: right;
}

/* The tick is the only place a completed lesson shows its course colour — a small reward. */
.lesson-rail .done { margin-left: auto; color: var(--course-solid); font-weight: 700; }

/* ---------- lesson ---------- */

.crumbs { color: var(--muted); font-family: var(--mono); font-size: .78rem; letter-spacing: .04em; margin-bottom: var(--space-3); }

/*
 * Was plain inline text at 28-39x17px — the smallest tap target on the site. A breadcrumb
 * trail is navigation, not prose, so it gets the same 44px floor as every other control.
 */
.crumbs a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: var(--space-2);
    color: var(--muted);
}

/*
 * The lesson keeps its own width; the contents menu lives OUTSIDE it, in the page margin.
 * The grid centres the pair, so the reading column is unchanged whether or not a lesson has
 * enough headings to earn a menu — a menu that narrowed the prose would make the same lesson
 * a different width depending on how it was written.
 */
.lesson {
    max-width: 100%;
    display: grid;
    grid-template-columns: minmax(0, 940px) 250px;
    justify-content: center;
    column-gap: var(--space-6);
    align-items: start;
}

.lesson > :not(.lesson-toc) { grid-column: 1; }

/*
 * :has() lets the lesson page widen its own container without a class on <main>, which the
 * layout owns and the view cannot reach. Where :has() is unsupported the page simply stays at
 * the normal width and the menu tucks in beside a narrower column — degraded, not broken.
 */
.site-main:has(.lesson) { max-width: 1320px; }

/*
 * Bare text with a rule down its edge, not a panel. align-self:start is what makes sticky work
 * in a grid: a stretched item already fills its row and so has nothing to travel within.
 */
.lesson-toc {
    grid-column: 2;
    grid-row: 1 / -1;
    align-self: start;
    position: sticky;
    top: var(--space-6);
    max-height: calc(100vh - var(--space-8));
    overflow-y: auto;
    min-width: 0;
}

.toc-title {
    font-family: var(--mono);
    font-size: .7rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 var(--space-3);
    padding-left: var(--space-4);
}

.lesson-toc ol { list-style: none; margin: 0; padding: 0; }

.lesson-toc a {
    display: block;
    padding: .4rem 0 .4rem var(--space-4);
    /*
     * The rule is on every item, not just the current one. Colouring a shared rule is a change
     * of state; growing a border only on the active item shifts every other row sideways.
     */
    border-left: 2px solid var(--border);
    color: var(--muted);
    font-size: .85rem;
    line-height: 1.45;
    text-decoration: none;
    transition: color .12s, border-color .12s;
}

.lesson-toc a:hover { color: var(--text); border-left-color: var(--border-strong); }

.lesson-toc a.is-current {
    color: var(--text);
    border-left-color: var(--course-solid);
    font-weight: 500;
}

/* A heading jumped to must clear the top of the viewport rather than sit flush against it. */
.block-island.is-heading { scroll-margin-top: var(--space-5); }

/*
 * The head is an island too. Once every block below it sits on a panel, the one bare element
 * on the page reads as unstyled rather than as a deliberate exception — and the title is a
 * block of the lesson like any other. The crumbs stay off the field on purpose: they are
 * navigation about the page, not part of it.
 */
.lesson-head {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
    padding: var(--space-5) var(--space-5) var(--space-4);
    margin-bottom: var(--space-3);
}

.lesson-head > :last-child { margin-bottom: 0; }
.lesson-head h1 { font-size: clamp(1.9rem, 4vw, 2.8rem); }

/*
 * Islands, applied to the lesson body: every content block is its own soft panel, and the
 * page becomes a field of discrete pieces rather than one continuous column.
 *
 * Line length is governed by the lesson column's own width rather than a ch cap, so prose
 * lines up with the editor, quiz and task panels. That buys visual consistency at the cost of
 * a longer measure than the 72ch ideal — worth knowing if these readers, beginners working in
 * a second language, ever report losing their place between lines.
 */
.lesson-body {
    /*
     * Full width of the lesson column, matching the editor, quiz and task panels below it.
     * A capped measure centred inside a wider column left the prose floating with gaps on both
     * sides while the panels ran edge to edge — the page read as two different layouts stacked.
     * The trade is a longer line than the 72ch ideal; the island padding claws some of it back.
     */
    display: grid;
    /*
     * minmax(0, 1fr), not the implicit 1fr. A grid item defaults to min-width:auto, so it
     * refuses to shrink below its content's min-content width and pushes the track wider than
     * the viewport — measured at 536px against a 375px screen before this was added. Lesson
     * text carries inline <code> with unbreakable tag names, which is exactly what triggers it.
     */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
}

.block-island {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
    padding: var(--space-4) var(--space-5);
    min-width: 0;
}

/* A long tag name or URL inside prose breaks rather than widening its island. */
.block-island :not(pre) > code { overflow-wrap: anywhere; }

/* The island owns the outer spacing now, so the block inside it must not add its own. */
.block-island > :first-child { margin-top: 0; }
.block-island > :last-child { margin-bottom: 0; }

/*
 * A heading is a label for the blocks beneath it, not a peer of them, so it keeps the island
 * geometry but drops the fill and the border. Give it the same panel treatment as a paragraph
 * and a lesson reads as a flat list of equal things, with nothing announcing a new section.
 */
.block-island.is-heading {
    background: transparent;
    border-color: transparent;
    padding-bottom: 0;
    margin-top: var(--space-4);
}

.block-island.is-heading:first-child { margin-top: 0; }

/*
 * Code: the island supplies the frame, so the pre drops its own border and corner. Two nested
 * borders 20px apart read as a mistake.
 */
.block-island.is-code { padding: var(--space-3); }
.block-island.is-code .block-code { margin: 0; }
.block-island.is-code .block-code pre { border: 0; border-radius: var(--radius); }
.block-island.is-code figcaption { padding: var(--space-3) var(--space-2) var(--space-1); }

/*
 * Callouts: the island IS the callout. The inner element keeps only its icon-and-text layout
 * and hands its frame to the panel, which also carries the course gradient down the edge.
 */
.block-island.is-tip, .block-island.is-warn { position: relative; overflow: hidden; padding-left: calc(var(--space-5) + 3px); }
.block-island.is-tip::before, .block-island.is-warn::before {
    content: '';
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--course-grad);
}
.block-island.is-warn::before { background: var(--warn); }
.block-island.is-tip .callout, .block-island.is-warn .callout {
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 0;
    margin: 0;
}
.block-island.is-tip .callout::before, .block-island.is-warn .callout::before { display: none; }

/* Tables bring their own ruled grid; the island just frames it. */
.block-island.is-table { padding: var(--space-3); }
.block-island.is-table .block-table { margin: 0; }

/*
 * Figures: the island frames them, the same as code and tables.
 *
 * .figure-frame is a mat, and it is doing real work rather than decorating. A screenshot or a
 * generated illustration carries its own baked-in background, and a white one dropped straight
 * onto --bg glares. The mat puts a deliberate step between page and image instead of a hard
 * white edge. Auto-inverting the image was the alternative and is always wrong somewhere: a
 * screenshot of a light-themed editor is not improved by being turned inside out.
 */
.block-island.is-figure { padding: var(--space-3); }

.figure { margin: 0; }

.figure-frame {
    display: block;
    width: 100%;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3);
    /* The zoomable variant is a <button>, which would otherwise arrive with UA styling. */
    font: inherit;
    color: inherit;
    text-align: center;
}

.figure-frame img,
.figure-frame svg { display: block; max-width: 100%; height: auto; margin-inline: auto; }

/*
 * .lesson-body is one full-width column, so `wide` is not about escaping a narrower measure —
 * it is about whether the drawing is allowed to use the whole island. A screenshot capped at a
 * comfortable reading width is unreadable, and its detail is the entire reason it is there.
 */
.figure.size-inline .figure-frame { max-width: 40rem; margin-inline: auto; }

button.figure-frame { cursor: zoom-in; }
button.figure-frame:hover { border-color: var(--border-strong); }
button.figure-frame:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.figure figcaption {
    color: var(--muted);
    font-size: .9rem;
    margin-top: var(--space-2);
    padding-inline: var(--space-2);
}

/*
 * An animation must be correct when frozen, not merely still. Motion here is decoration on top
 * of a drawing that already says the whole thing — so stopping it costs a reader the emphasis
 * and none of the meaning. Authoring rule as much as a stylesheet rule.
 */
@media (prefers-reduced-motion: reduce) {
    .figure.is-animation svg * { animation: none; }
}

/* The zoom overlay. Its own thing rather than the admin lightbox, which exists to display
   untrusted student uploads and carries sandboxed frames and archive listing to do it. */
.figure-zoom-box {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    place-items: center;
    padding: var(--space-5);
    background: var(--scrim);
}

.figure-zoom-box img {
    max-width: 100%;
    max-height: 100%;
    border-radius: var(--radius);
    background: var(--surface-2);
}

.has-figure-zoom { overflow: hidden; }

/* A revealed Arabic note sits on the step-up surface so it reads as a layer on its block. */
.block-island .note { background: var(--surface-2); border-radius: var(--radius); }
.lead { font-size: 1.12rem; color: var(--text); max-width: 66ch; }
.muted { color: var(--muted); }

.block-heading { margin-top: var(--space-7); font-size: 1.35rem; }
.block-text { margin: 1rem 0; }
.block-list { margin: 1rem 0; padding-left: 1.3rem; }
.block-list li { margin: .4rem 0; }

.block-code { margin: var(--space-5) 0; }

.block-code pre {
    background: var(--code-bg);
    color: var(--code-text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    overflow-x: auto;
    margin: 0;
}

.block-code figcaption { color: var(--muted); font-size: .9rem; margin-top: var(--space-2); }

/*
 * The left rule is the course gradient rather than a flat accent, so a callout inside an HTML
 * lesson and one inside a CSS lesson are visibly of their own course.
 *
 * Drawn as a pseudo-element, NOT with border-image. `border-image` replaces all four borders
 * with slices of the image — it cannot paint only the left edge — so the obvious spelling
 * (border-left: 3px + border-image: <gradient> 1) puts the gradient around the whole box and
 * throws the 1px --border away with it.
 */
.callout {
    position: relative;
    display: flex;
    gap: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    padding-left: calc(var(--space-4) + 3px);
    margin: var(--space-5) 0;
}

.callout::before {
    content: '';
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--course-grad);
}

.callout.warn::before { background: var(--warn); }
.callout-icon { display: flex; padding-top: .15rem; color: var(--course-solid); }
.callout.warn .callout-icon, .block-island.is-warn .callout-icon { color: var(--warn); }

.block-table { margin: var(--space-5) 0; overflow-x: auto; }
.block-table table { border-collapse: collapse; width: 100%; }
.block-table th, .block-table td { border: 1px solid var(--border); padding: .55rem .8rem; text-align: left; }
.block-table th { background: var(--surface-2); font-family: var(--mono); font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.table-caption { color: var(--muted); font-size: .92rem; }

.note-btn {
    background: transparent;
    border: 0;
    cursor: pointer;
    font-size: .95rem;
    padding: 0 .2rem;
    color: var(--muted);
    opacity: .75;
    transition: opacity .15s;
}

.note-btn:hover, .note-btn.is-open { opacity: 1; color: var(--accent); }

.note {
    background: var(--accent-soft);
    border-right: 3px solid var(--accent);
    border-radius: var(--radius);
    padding: .8rem 1rem;
    margin: .5rem 0 1rem;
    font-size: .97rem;
    line-height: 1.9;
}

.try-btn {
    background: var(--accent-soft);
    color: var(--accent);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    padding: .25rem .7rem;
    font: inherit;
    font-size: .85rem;
    cursor: pointer;
    margin-left: .5rem;
}

.lesson-nav { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); margin-top: var(--space-7); }
.nav-island { padding: var(--space-4); text-decoration: none; color: inherit; }
.nav-island .eyebrow { margin-bottom: var(--space-1); display: block; }
.nav-island.is-next { text-align: right; }

/* ---------- editor ---------- */

.editor { margin: 3rem 0; border: 1px solid var(--border); border-radius: var(--radius-island); background: var(--surface); }

.editor-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; flex-wrap: wrap; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border);
}

.editor-head h2 { margin: 0; }
.editor-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.editor-actions .btn { padding: .35rem .8rem; font-size: .85rem; }

.editor-grid { display: grid; grid-template-columns: 1fr 1fr; min-height: 320px; }
.editor-panes { display: flex; flex-direction: column; border-right: 1px solid var(--border); }
.editor-tabs { display: flex; border-bottom: 1px solid var(--border); }

.editor-tabs .tab {
    background: transparent; border: 0; border-bottom: 2px solid transparent;
    color: var(--muted); padding: .55rem 1rem; font: inherit; font-size: .85rem; cursor: pointer;
}

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

/*
 * The mirror sits UNDER a transparent-text textarea. Every metric that affects glyph
 * position must come from the same declarations — not merely equal values, the same ones —
 * or the two drift apart at some font size on some platform.
 */
.code-pane, .code-mirror {
    font-family: var(--mono);
    /* Also a metric: a ligature substitutes glyphs, and the two layers must substitute
       identically or the highlight drifts off the characters it is colouring. */
    font-variant-ligatures: none;
    font-feature-settings: "liga" 0, "calt" 0;
    font-size: .88rem;
    line-height: 1.6;
    padding: var(--space-4);
    border: 0;
    tab-size: 2;
    white-space: pre-wrap;
    overflow-wrap: break-word;
    letter-spacing: normal;
}

.code-pane {
    width: 100%; resize: vertical;
    background: var(--surface-2); color: var(--text);
}

.code-pane:focus { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The wrapper is the positioning context, NOT .editor-panes — that also holds the tab row. */
.pane-wrap { position: relative; flex: 1; display: flex; min-height: 280px; }
.pane-wrap > .code-pane { flex: 1; }

.code-mirror {
    position: absolute;
    inset: 0;
    margin: 0;
    overflow: auto;
    background: var(--code-bg);
    color: var(--code-text);
    pointer-events: none;
}

.editor.is-highlighted .code-pane {
    position: relative;
    background: transparent;
    color: transparent;
    /* var(--text) on var(--code-bg) is 1.12:1 — the mirror is always dark, so the caret
       needs the code theme's own text colour, not the page theme's. */
    caret-color: var(--code-text);
    /* Dragging the resize grip would grow the textarea alone, leaving transparent text
       rendered over nothing below the wrapper/mirror's unchanged height. */
    resize: none;
}

/*
 * `visibility: hidden`, not `display: none`. The self-check measures scrollHeight and a
 * span's getBoundingClientRect() on this element BEFORE .is-highlighted is added — a
 * display:none ancestor collapses both to zero and the check would fail every time, on
 * every browser, regardless of actual alignment. Visibility keeps the layout box (so the
 * measurement is real) while keeping it unseen; the opaque textarea sits on top of it
 * regardless, so nothing shows through either way.
 */
.editor:not(.is-highlighted) .code-mirror { visibility: hidden; }

.editor-preview { display: flex; flex-direction: column; background: var(--preview-bg); }
.preview-label { background: var(--surface-2); color: var(--muted); font-size: .78rem; padding: .35rem .8rem; }
.preview-frame { flex: 1; width: 100%; border: 0; background: var(--preview-bg); min-height: 280px; }

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

/*
 * The playground is a workspace, not an article, so it gets its own width and fills the
 * screen it is on.
 *
 * The height comes from a flex chain that already exists — body is a column with
 * min-height:100dvh and .site-main takes the slack — rather than from calc(100vh - 260px).
 * A magic number there has to be re-derived every time the header, the page padding or this
 * page's own heading changes, and it is wrong on the first browser whose chrome differs.
 */
.site-main:has(.playground) { max-width: 1500px; display: flex; flex-direction: column; }

.playground { flex: 1; display: flex; flex-direction: column; min-width: 0; }

/*
 * Kept deliberately short. Every pixel this row takes is a pixel of editor, and on a 13"
 * laptop the header, this heading and the footer together were pushing the workspace off the
 * bottom of the screen — measured at 888px of content in an 813px window before trimming.
 */
.playground-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-5);
    margin-bottom: var(--space-3);
}

/*
 * The intro sits beside the title, not pushed to the far edge. space-between looks balanced
 * on a narrow page and marooned on a 1500px one — the text ends up 700px from the heading it
 * belongs to, aligned with nothing, while the pair reads as two unrelated blocks.
 */
.playground-intro { flex: 0 1 auto; }

.playground-title .eyebrow { margin-bottom: var(--space-1); }
.playground-title h1 { margin: 0; font-size: clamp(1.4rem, 2.1vw, 1.85rem); line-height: 1.2; }
.playground-intro { max-width: 52ch; }
.playground-intro .lead { margin: 0; font-size: .95rem; line-height: 1.55; }
.playground-intro .ar { font-size: .9rem; }

/*
 * The editor takes every row the head does not. min-height is the floor for a short window;
 * min-height:0 on the children is what lets them shrink INSIDE it — a flex or grid child
 * defaults to min-height:auto and refuses to go below its content, which would push the
 * preview off the bottom of the screen instead of scrolling within its own pane.
 */
/*
 * The floor is 340px, not 440: a floor taller than the space a short laptop actually has
 * forces the whole page to scroll, which on a workspace means the preview leaves the screen
 * exactly when you are watching it change.
 */
.playground .editor { flex: 1; margin: 0; display: flex; flex-direction: column; min-height: 340px; }
.playground .editor-grid { flex: 1; min-height: 0; }
.playground .editor-panes, .playground .editor-preview { min-height: 0; }
.playground .pane-wrap { min-height: 0; }
.playground .preview-frame { min-height: 0; }

/* A wider workspace can afford to give the preview more room than the code it renders. */
@media (min-width: 1100px) {
    .playground .editor-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
}

/*
 * Below 800px the editor grid stacks, and a filled height would then split one screen
 * between two panes and leave neither usable. Let the page grow and scroll instead.
 */
@media (max-width: 800px) {
    .playground .editor { flex: none; min-height: 0; }
    .playground .pane-wrap { min-height: 280px; }
    .playground .preview-frame { min-height: 280px; }
}

.editor.is-fullscreen {
    position: fixed; inset: 0; z-index: 50; margin: 0; border-radius: 0; overflow: auto;
}

.editor.is-fullscreen .editor-grid { min-height: calc(100vh - 70px); }

@media (max-width: 800px) {
    .editor-grid { grid-template-columns: 1fr; }
    .editor-panes { border-right: 0; border-bottom: 1px solid var(--border); }
}

/*
 * The label bar hosts controls as well as a title, so it needs to lay them out — the base
 * .island-label is a single flex row and would otherwise stack the buttons against the text.
 */
.editor-head.island-label {
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/*
 * The quiz and task labels are <h2> elements, so they arrive carrying the display face, 700
 * weight and negative tracking from the heading scale. A label bar wants none of that — this
 * resets them to the same mono chrome the editor's bar uses.
 */
.editor-head.island-label h2,
.quiz > .island-label,
.task > .island-label {
    margin: 0;
    font-family: var(--mono);
    font-size: .72rem;
    font-weight: 400;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}

/* Inside a label bar the buttons are chrome, not page actions — smaller, and never taller
   than the bar. The 44px floor still applies, so the bar sets its own min-height to match. */
.island-label .btn { padding: .3rem .7rem; font-size: .78rem; text-transform: none; letter-spacing: normal; min-height: 32px; }
.editor-head.island-label { min-height: 44px; }

.editor { overflow: hidden; }
.quiz-option { border-radius: var(--radius); }
.quiz-num { font-family: var(--mono); color: var(--muted); }
.quiz-score { font-family: var(--mono); }

/* ---------- quiz ---------- */

/* No padding of its own: the content sits in .island-body, and a padded island would inset
   its own .island-label band instead of letting it span the top edge. Same for .task. */
.quiz { margin: 3rem 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-island); }
.quiz-item { padding: 1rem 0; border-top: 1px solid var(--border); }
.quiz-item:first-of-type { border-top: 0; }
.quiz-q { margin: 0 0 .75rem; font-weight: 600; }
.quiz-num { color: var(--muted); margin-right: .3rem; }
.quiz-options { display: grid; gap: .5rem; }

.quiz-option {
    text-align: left;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    padding: .6rem .9rem;
    font: inherit;
    font-family: var(--mono);
    font-size: .9rem;
    cursor: pointer;
}

.quiz-option:hover:not(:disabled) { border-color: var(--accent); }
.quiz-option:disabled { cursor: default; }
.quiz-option.is-correct { border-color: var(--ok); background: var(--ok-soft); }
.quiz-option.is-wrong { border-color: var(--bad); background: var(--bad-soft); }
.quiz-explain { margin-top: .75rem; font-size: .95rem; color: var(--muted); }
.quiz-score { margin: 1rem 0 0; font-weight: 600; }

/* ---------- task form ---------- */

.task { margin: 3rem 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-island); }
.task-brief { margin: .5rem 0; }
.checklist { margin: 1rem 0; padding-left: 1.2rem; color: var(--muted); font-size: .95rem; }
.checklist .ar { display: block; }

.task-form { display: grid; gap: 1rem; margin-top: 1.5rem; }
.field-row { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; }
.field { display: grid; gap: .35rem; font-size: .9rem; }
.field > span { color: var(--muted); }

.field input[type="text"], .field input[type="email"], .field input[type="file"], .field input[type="password"] {
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    padding: .6rem .8rem;
    font: inherit;
}

.mode-picker { border: 1px solid var(--border-strong); border-radius: var(--radius); padding: .8rem 1rem; }
.mode-picker legend { color: var(--muted); font-size: .85rem; padding: 0 .4rem; }

/*
 * The radio circle itself stays native size (13x13) — the label text is its tap target, per
 * the standard WCAG technique for form controls with an associated <label>. min-height keeps
 * that target >=44px even for the one-line "Upload a file" option, which otherwise measured
 * 292x26.
 */
.mode-picker label { display: block; margin: .3rem 0; padding: .4rem 0; min-height: 44px; font-size: .93rem; }

.hp { position: absolute; left: -9999px; }

.form-message { margin: 0; padding: .7rem 1rem; border-radius: var(--radius); font-size: .93rem; }
.form-message.is-ok { background: var(--ok-soft); color: var(--ok); }
.form-message.is-bad { background: var(--bad-soft); color: var(--bad); }

/* ---------- admin ---------- */

.admin-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.admin-head h1 { margin: 0; }

/* The row count belongs to the heading, so it is set against the heading rather than beside
   it in parentheses — a parenthesis is punctuation doing a label's job. */
.count-pill {
    display: inline-block;
    vertical-align: middle;
    margin-left: var(--space-2);
    padding: .1rem .6rem;
    border-radius: var(--radius-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: .85rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    font-family: var(--mono);
}

/*
 * The filters are a toolbar, so they get a toolbar's container. Loose on the page they read
 * as three unrelated fields that happen to sit in a row.
 */
.admin-filters {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    margin: var(--space-5) 0;
    padding: .8rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: none;
}

.admin-filters input, .admin-filters select {
    background: var(--surface-2); border: 1px solid var(--border-strong); border-radius: var(--radius);
    color: var(--text); padding: .5rem .7rem; font: inherit; font-size: .9rem;
    transition: border-color .15s;
}

/* Grow to fill the bar, but min-width:0 so a long placeholder cannot push the button off the
   end — flex items default to min-width:auto and refuse to shrink below their content. */
.admin-filters input[type="text"] { flex: 1 1 11rem; min-width: 0; }
.admin-filters input:hover, .admin-filters select:hover { border-color: var(--accent); }
.admin-filters .btn { margin-left: auto; }

/* ---------- admin table ---------- */

/*
 * Scoped to .admin-table, not .block-table: the same wrapper class carries the reference
 * tables inside lessons, and those are prose furniture that should stay quiet. This one is
 * a working surface someone scans for minutes at a time.
 */
.admin-table {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-island);
    box-shadow: none;
}

/*
 * Ruled rows, not a grid of boxes. Vertical lines in a data table draw cells; the eye is
 * reading across a record, and the column gaps already separate the fields.
 */
.admin-table th, .admin-table td {
    border: 0;
    border-bottom: 1px solid var(--border);
    padding: .8rem 1rem;
    vertical-align: middle;
}

.admin-table thead th {
    background: var(--surface-2);
    color: var(--muted);
    font-size: .74rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .07em;
    white-space: nowrap;
    font-family: var(--mono);
}

.admin-table tbody tr { transition: background-color .12s; }
.admin-table tbody tr:hover { background: var(--surface-2); }
.admin-table tbody tr:last-child td { border-bottom: 0; }

.admin-table .col-id a {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-decoration: none;
}

.admin-table .col-id a:hover { text-decoration: underline; }

/* Names sit above their address; the address is a lookup key, not a second line of prose. */
.admin-table .col-student small { font-size: .82rem; }

/* Tabular figures so ids, scores and timestamps line up as columns of digits. */
.admin-table .col-score, .admin-table .col-date { font-variant-numeric: tabular-nums; white-space: nowrap; }
.admin-table .col-date { color: var(--muted); font-size: .88rem; }

/* A slug is an identifier the reader may have to type back into the filter above. Setting it
   in the mono face says that, and makes a transposed character visible. */
.admin-table .col-slug { font-family: var(--mono); font-size: .85rem; }

.admin-empty td { padding: var(--space-7) var(--space-4); text-align: center; color: var(--muted); }

/* ---------- admin chips ---------- */

.chip {
    display: inline-block;
    padding: .15rem .55rem;
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    color: var(--muted);
    font-family: var(--mono);
    font-size: .72rem;
    letter-spacing: .04em;
    font-weight: 600;
    white-space: nowrap;
}

/* The course is the one field in the row that is also a filter, so it is the one that earns
   colour. Everything else stays neutral or the table turns into bunting. */
.chip.is-course { background: var(--accent-soft); color: var(--accent); }

/* ---------- admin detail ---------- */

.admin-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin: var(--space-3) 0 0; }

/* Without a fill of its own the slug runs straight into the timestamp beside it — same
   colour, same size, and a gap that reads as a word space rather than a field boundary. */
.admin-meta code {
    background: var(--surface-2);
    border-radius: var(--radius);
    padding: .15rem .45rem;
    font-size: .85rem;
}

.admin-preview { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 1.5rem; }

.admin-pane {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    padding: 1.1rem;
    /* min-width:0 or a long unbroken code line makes this grid column wider than its track */
    min-width: 0;
}

.admin-pane > h2 {
    margin: 0 0 var(--space-3);
    font-size: .78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--muted);
}

.admin-preview .preview-frame { min-height: 400px; border: 1px solid var(--border); border-radius: var(--radius); }
.admin-preview pre { max-height: 400px; overflow: auto; }
.admin-pane .block-code { margin: 0 0 var(--space-4); }
.admin-pane .block-code:last-child { margin-bottom: 0; }

.file-list { list-style: none; padding: 0; display: grid; gap: .5rem; }

.file-list li {
    display: flex;
    gap: .8rem;
    align-items: baseline;
    flex-wrap: wrap;
    padding: .7rem .9rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: none;
}

.file-list li > span { margin-left: auto; font-size: .85rem; }

.pager { display: flex; gap: .4rem; margin-top: 1.5rem; flex-wrap: wrap; }

.pager a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Same 44px floor every other control on the site keeps. */
    min-width: 44px;
    min-height: 44px;
    padding: 0 .7rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: none;
    text-decoration: none;
    font-variant-numeric: tabular-nums;
    transition: border-color .15s, background-color .15s;
}

.pager a:hover { border-color: var(--accent); }
.pager a.is-current { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }

/* ---------- admin login ---------- */

/*
 * .narrow centres its text, which is right for the 404 and 500 pages it also dresses and
 * wrong for a form: a centred label floating over a full-width input belongs to neither
 * edge of the field it names.
 */
.admin-login { max-width: 400px; text-align: left; box-shadow: var(--shadow-3); }
.admin-login h1 { margin: 0; text-align: center; font-size: 1.4rem; }
.admin-login .task-form { margin-top: var(--space-5); }
.admin-login .btn { width: 100%; }

@media (max-width: 800px) { .admin-preview { grid-template-columns: 1fr; } }

.lesson.is-complete .lesson-head::after {
    content: '✓ Completed / مكتمل';
    display: inline-block;
    background: var(--ok-soft);
    color: var(--ok);
    border-radius: var(--radius-pill);
    padding: .2rem .7rem;
    font-size: .8rem;
}

/* ---------- theme toggle ---------- */

.theme-toggle {
    background: transparent;
    /*
     * --border-strong, not --border. This is a real control, and WCAG 1.4.11 wants 3:1 on a
     * control's boundary. Measured against the header's --surface, --border is 1.30:1 in dark
     * and 1.36:1 in light — the button's edge is essentially invisible until you hover, which
     * does nothing for keyboard or touch users. The contrast test cannot catch this because it
     * deliberately exempts --border as decorative; the token was simply used in the wrong place.
     */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
    /* 44px minimum touch target — the icon stays small, the hit area does not */
    min-width: 44px;
    min-height: 44px;
    font-size: 1rem;
    line-height: 1;
}

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

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

@font-face {
    font-family: "IBM Plex Sans Arabic";
    src: url("../fonts/IBMPlexSansArabic-Regular.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "IBM Plex Sans Arabic";
    src: url("../fonts/IBMPlexSansArabic-SemiBold.woff2") format("woff2");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

/*
 * One variable file covers every weight the design uses (400 body, 500 labels, 700 h2,
 * 800 display). Four static Inter files would have been 455KB for the same coverage.
 */
@font-face {
    font-family: "Inter";
    src: url("../fonts/InterVariable-subset.woff2") format("woff2");
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "JetBrains Mono";
    src: url("../fonts/JetBrainsMono-Regular-subset.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "JetBrains Mono";
    src: url("../fonts/JetBrainsMono-Bold-subset.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* ---------- bilingual ---------- */

.bilingual { margin: var(--space-4) 0; }
.bilingual > :first-child { margin: 0; }

/*
 * The Arabic sits directly under its English in the SAME column, tied by a rule on its
 * start edge. Previously `.ar { text-align: right }` pushed translations to the far edge
 * of a 1100px container, visually orphaning them from the text they translate.
 */
.bilingual > .ar,
.note,
[lang="ar"] {
    font-family: var(--arabic);
}

.bilingual > .ar {
    direction: rtl;
    text-align: right;
    margin: var(--space-2) 0 0;
    padding-right: var(--space-3);
    border-right: 2px solid var(--accent-soft);
    font-size: 1.05em;      /* Arabic reads small next to Latin at the same nominal size */
    line-height: 1.9;       /* and needs more vertical room */
    color: var(--muted);
}

/* ---------- touch targets ---------- */

/*
 * The note button is the site's primary interaction and measured 25x25 — unusable on a
 * phone, which is what most students are holding. The icon stays small; the hit area does
 * not. Negative margin keeps the larger box from disturbing the text it sits in.
 */
.note-btn {
    min-width: 44px;
    min-height: 44px;
    margin: -10px -6px;
    vertical-align: middle;
}

.try-btn,
.tab,
.quiz-option,
.editor-actions .btn {
    min-height: 44px;
}

.lesson-list a { min-height: 44px; }

/* ---------- control boundaries ---------- */

/*
 * These three are controls whose border is the ONLY thing identifying them, so WCAG 1.4.11's
 * 3:1 applies. Measured: .quiz-option's fill is 1.09:1 against its container in dark and
 * 1.11:1 in light — it does no identifying work at all; .btn.ghost and .pager a have no fill.
 *
 * Deliberately NOT included: .lesson-list a and .course-card. They are content blocks
 * identified by their text, not compact controls, and bordering every card at 3:1 produces
 * exactly the heavy, boxy look this phase is trying to avoid.
 */
.btn.ghost,
.quiz-option,
.pager a { border-color: var(--border-strong); }

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

/*
 * Sized in em so an icon matches whatever text it sits beside, and flex:none so it never
 * gets squeezed when its row runs out of room. stroke-width scales with the box, which is
 * why a 24-unit drawing stays crisp at any of these sizes.
 */
.icon {
    width: 1.15em;
    height: 1.15em;
    flex: none;
    vertical-align: -.2em;
}

/*
 * Both theme icons are in the DOM and CSS picks one. The alternative — writing the SVG from
 * theme.js — would duplicate the icon set into a script AND flash the wrong glyph, because the
 * pre-paint theme script sets the attribute long before theme.js loads.
 */
.theme-icon { display: inline-flex; }
/* Light is the default, so the sun shows unless the dark attribute is present. */
.theme-icon .icon-moon { display: none; }
:root[data-theme="dark"] .theme-icon .icon-sun { display: none; }
:root[data-theme="dark"] .theme-icon .icon-moon { display: inline-block; }

/* Buttons whose label sits next to an icon line them up on one baseline row. */
.try-btn, .nav-island .eyebrow, .crumbs a { display: inline-flex; align-items: center; gap: .4rem; }
.done .icon { width: 1.1em; height: 1.1em; }

/*
 * The note button is a 44px hit area (see the touch-targets section) wrapping a much smaller
 * glyph, so it has to centre its own content — a default inline button would sit the icon on
 * the text baseline in the corner of that box.
 */
.note-btn { display: inline-flex; align-items: center; justify-content: center; }
.note-btn .icon { width: 1.3em; height: 1.3em; }

/* ---------- focus ---------- */

/*
 * Only the code panes had a focus style. Everything focusable needs one, and :focus-visible
 * means a mouse user never sees a ring they did not ask for.
 */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    /*
     * No border-radius here. CSS has no "outline radius": setting border-radius inside
     * :focus-visible changes the ELEMENT's corners, so every button and input would snap
     * from --radius (2px) to 4px the moment it received keyboard focus, and back on blur.
     * Modern browsers already clip the outline to the element's existing radius.
     */
}

/* ---------- motion ---------- */

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

.card-progress { margin-top: var(--space-3); }
.card-progress .progress-bar { height: 6px; }

/* ---------- syntax highlighting ---------- */

.block-code pre, .admin-preview pre { background: var(--code-bg); color: var(--code-text); }
.tok-tag     { color: var(--code-tag); }
.tok-attr    { color: var(--code-attr); }
.tok-value   { color: var(--code-value); }
.tok-comment { color: var(--code-comment); font-style: italic; }
.tok-punct   { color: var(--code-punct); }
.tok-text    { color: var(--code-text); }

/* ---------- responsive ---------- */

/*
 * This block lives last on purpose. A media query adds no specificity of its own, so when two
 * same-specificity rules for the same selector exist, the one that comes LATER in the file
 * wins — regardless of which one sits inside a media query. Placing this block anywhere before
 * a selector's base rule silently makes its override dead code. Keeping it last is the only
 * placement that stays correct as the file grows.
 */
/*
 * Below 1240px the margin cannot hold the menu beside a 940px column, so it stops being a
 * margin note: single column, static rather than sticky, above the lesson. Placed before the
 * 600px block so that block's rules still win where the two overlap.
 */
@media (max-width: 1240px) {
    .lesson { grid-template-columns: minmax(0, 1fr); }
    .lesson > :not(.lesson-toc) { grid-column: 1; }
    .site-main:has(.lesson) { max-width: 1100px; }
    .lesson { grid-template-columns: minmax(0, 1fr); }
    .lesson-toc {
        grid-column: 1;
        grid-row: auto;
        position: static;
        max-height: none;
        order: -1;
        margin-bottom: var(--space-5);
    }
}

@media (max-width: 600px) {
    .site-main { padding: 1.25rem 0 3rem; }

    /*
     * Under 600px a bordered island wastes horizontal space on two side borders and two
     * gutters. Dropping the sides turns each island into a full-bleed band, which is both
     * roomier and a stronger read — the band edges still separate them.
     */
    .island, .lesson-rail {
        border-left: 0;
        border-right: 0;
        border-radius: 0;
    }

    .lesson-nav { grid-template-columns: 1fr; }

    .nav-island.is-next { text-align: left; }

    /*
     * Text still needs its side padding even when the island's border is gone — but nothing
     * that CONTAINS an island may be padded, or it insets the island 1rem while the island's
     * own side borders are stripped, leaving it neither bordered nor full-bleed.
     *
     * .course-grid is such a container on the home page. So are the course page's
     * <div data-course> and the lesson page's <article class="lesson" data-course>, which is
     * why both are excluded from the first rule; the second rule then pads their text children
     * instead, skipping .lesson-nav because it holds the two nav islands.
     *
     * :not(.island) on that wrapper matters for the playground, where the editor island is
     * itself a direct child of .site-main and carries data-course — without it the editor's
     * own head and grid would take side padding and lose their full-bleed edges.
     */
    .site-main > :not(.island):not(.lesson-rail):not(.course-grid):not([data-course]) { padding-inline: 1rem; }
    .site-main > [data-course]:not(.island) > :not(.island):not(.lesson-rail):not(.lesson-nav) { padding-inline: 1rem; }

    /* Folded in from the task form's own mobile override, so the 600px breakpoint lives in
       one place instead of being split across the file. */
    .field-row { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
 * Course foundation: chapters, course facts, marks, search.
 * Colour comes from tokens only — tests/tokens_test.php fails the build on a literal.
 * ------------------------------------------------------------------------- */

/*
 * The course mark. Inlined SVG, so it inherits the course colour through --course-solid
 * exactly as a lesson diagram does; it is sized here rather than in the file so one drawing
 * can serve both the wide course header and the small home card.
 */
.course-mark { display: inline-flex; flex: none; }
.course-mark svg { width: 100%; height: 100%; display: block; }

.course-head { display: flex; align-items: flex-start; gap: var(--space-5); margin-bottom: var(--space-6); }
.course-head .course-mark { width: 3.5rem; height: 3.5rem; margin-top: var(--space-2); }
.course-head-text { min-width: 0; }

.course-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-5);
    list-style: none;
    padding: 0;
    margin: var(--space-5) 0 0;
}

.course-facts li { display: flex; flex-direction: column; gap: var(--space-1); }
.fact-label { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; }
.course-facts strong { font-size: 1rem; text-transform: capitalize; }

.course-requires { margin-top: var(--space-4); color: var(--muted); font-size: .92rem; }

.course-outcomes { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-3); }
.course-outcomes li { display: flex; gap: var(--space-3); align-items: flex-start; }
/* The tick is the only thing carrying the course's colour here, so the list reads as this
   course's promises rather than as generic chrome. */
.course-outcomes .icon { color: var(--course-solid, var(--accent)); flex: none; margin-top: .15em; }
.outcome-ar { display: block; color: var(--muted); font-size: .92rem; }

/*
 * A chapter is a heading over a rail of lessons, not a panel around them. Boxing each chapter
 * would put a frame inside a frame — the lesson rows are already bounded — and a 43-lesson
 * course would read as eight stacked cards rather than one list with signposts.
 */
.chapter { margin-top: var(--space-6); }

.chapter-title {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    font-size: 1.1rem;
    margin: 0 0 var(--space-3);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
}

.chapter-number {
    flex: none;
    width: 1.6rem;
    height: 1.6rem;
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    background: var(--course-grad, var(--accent));
    color: var(--accent-text);
    font-size: .8rem;
    font-weight: 700;
}

.chapter-count { margin-inline-start: auto; color: var(--muted); font-weight: 400; font-size: .85rem; }

/* Home cards: a small mark and the two facts worth knowing before opening a course. */
.course-card .course-mark { width: 2rem; height: 2rem; margin-bottom: var(--space-3); }
.course-chips { display: flex; gap: var(--space-2); margin: 0 0 var(--space-3); }

.chip {
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    padding: .1rem var(--space-3);
    font-size: .75rem;
    color: var(--muted);
    text-transform: capitalize;
}

/* Revealed by continue.js only when the browser has progress to resume from. */
.continue { margin: var(--space-5) 0 0; }
.continue a { font-weight: 600; }

.search-page { max-width: 52rem; }
.search-form { display: flex; gap: var(--space-3); margin-bottom: var(--space-4); }

.search-form input {
    flex: 1;
    min-width: 0;
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    padding: .55rem var(--space-4);
    font: inherit;
}

.search-form input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.search-scope { font-size: .85rem; margin: 0 0 var(--space-5); }
.search-count { margin: 0 0 var(--space-3); font-size: .9rem; }
.search-empty { margin: var(--space-5) 0; }

.search-results { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-3); }
.search-results li { padding: 0; }
.search-results a { display: block; padding: var(--space-4) var(--space-5); text-decoration: none; color: inherit; }
.search-results .eyebrow { margin-bottom: var(--space-1); }

@media (max-width: 40rem) {
    .course-head { gap: var(--space-4); }
    .course-head .course-mark { width: 2.5rem; height: 2.5rem; }
    .course-facts { gap: var(--space-4); }
}

/* ---------------------------------------------------------------------------
 * Paths: an ordered run of courses aimed at a job rather than a subject.
 * ------------------------------------------------------------------------- */

.path-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-5);
}

.path-card { text-decoration: none; color: inherit; }
.path-card h2 { font-size: 1.25rem; margin: 0 0 var(--space-2); }

.path-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    color: var(--muted);
    font-size: .85rem;
    margin: var(--space-4) 0 var(--space-3);
}

/*
 * A partial path must be legible as partial at a glance, not only once opened. --warn rather
 * than --bad: an unfinished path is a caveat, not a fault, and colouring it like an error would
 * tell a student to avoid the most useful thing on the page.
 */
.path-partial, .path-complete {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: .82rem;
    margin: 0;
}

.path-partial { color: var(--warn); }
.path-complete { color: var(--ok); }

.path-head { margin-bottom: var(--space-6); }
.path-summary { margin: var(--space-4) 0; max-width: 62ch; }

/*
 * The steps are a numbered list because the order IS the content — a path with its courses
 * shuffled is a different path. Counting comes from the markup rather than a CSS counter so the
 * number survives being read aloud.
 */
.path-steps { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-3); }

.path-step {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-4);
    align-items: start;
    padding: var(--space-4) var(--space-5);
}

.path-step-number {
    grid-row: 1 / span 2;
    width: 2rem;
    height: 2rem;
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    background: var(--course-grad, var(--accent));
    color: var(--accent-text);
    font-weight: 700;
    font-size: .9rem;
}

.path-step-body { display: flex; gap: var(--space-4); text-decoration: none; color: inherit; min-width: 0; }
.path-step-body .course-mark { width: 2rem; height: 2rem; flex: none; margin-top: .1rem; }
.path-step-text { min-width: 0; }
.path-step-facts { font-size: .82rem; margin: var(--space-2) 0 0; text-transform: capitalize; }
.path-cumulative { text-transform: none; }
.path-step-progress { grid-column: 2; margin-top: var(--space-3); }

/*
 * The gap panel is bordered in --warn rather than sitting quietly among the others. It is the
 * one thing on the page a student could be harmed by missing, and a panel that looks like every
 * other panel gets skimmed like every other panel.
 */
.path-gaps { border-color: var(--warn); }
.gap-list { margin: var(--space-4) 0 0; padding-left: 1.2rem; display: grid; gap: var(--space-3); }
.gap-ar { display: block; color: var(--muted); font-size: .9rem; }

/* Home: a short strip so a new student picks a direction before picking a subject. */
.path-strip { margin: var(--space-6) 0; }
.path-strip-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-3); }
.path-strip-head h2 { font-size: 1.15rem; margin: 0; }
.path-strip-all { display: inline-flex; align-items: center; gap: var(--space-2); font-size: .85rem; }

.path-strip-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: var(--space-3); }

.path-chip {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-4);
    text-decoration: none;
    color: inherit;
    font-size: .9rem;
}

.path-chip .muted { font-size: .8rem; }
.path-chip.is-partial { border-left: 3px solid var(--warn); }

.about-steps { margin: 0; padding-left: 1.3rem; display: grid; gap: var(--space-5); }
.about-ar { display: block; color: var(--muted); margin-top: var(--space-2); }
.about-paths { list-style: none; padding: 0; margin: var(--space-4) 0 0; display: grid; gap: var(--space-2); }

/* ---------------------------------------------------------------------------
 * Visual overhaul: the bilingual lockup, the sectioned home page, and motion.
 * ------------------------------------------------------------------------- */

/*
 * THE LOCKUP — the one thing this site is remembered by.
 *
 * Every title here exists twice, and it used to be set as "English / عربي": two strings joined
 * by a slash. A slash reads as a gloss, as though the Arabic were a parenthetical. It is not —
 * for half the people using this site the Arabic is the title.
 *
 * So they stack as siblings, each in its own script's face, joined by a short rule that carries
 * the course colour. The rule is the only ornament on the page that means something: it marks
 * the seam between two writing systems that run in opposite directions.
 */
.lockup { display: block; }
.lockup-en { display: block; }

.lockup-ar {
    display: block;
    font-family: var(--arabic);
    font-weight: 400;
    /* A step down and quieter, because the English sits first in the reading order here — not
       because it matters more. In an Arabic-first build these two declarations swap. */
    font-size: .82em;
    line-height: 1.7;
    color: var(--muted);
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    position: relative;
}

.lockup-ar::before {
    content: '';
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    width: 2.5rem;
    height: 2px;
    border-radius: var(--radius-pill);
    background: var(--course-grad, var(--accent));
}

.lockup.is-hero .lockup-en { font-size: clamp(2.2rem, 6vw, 3.6rem); letter-spacing: -.035em; line-height: 1.05; }
.lockup.is-hero .lockup-ar { font-size: clamp(1.1rem, 2.6vw, 1.5rem); }

h3.lockup .lockup-en, strong.lockup .lockup-en { font-size: 1.05rem; }
strong.lockup { font-weight: 700; }

/* The hero. The spectrum is the nine course colours, which are the site's real identity —
   already contrast-tuned, already meaningful, and impossible to lift from a template. */
.hero { position: relative; padding: var(--space-8) 0 var(--space-7); overflow: hidden; }

.hero-spectrum {
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    height: 4px;
    display: flex;
}

.hero-spectrum span { flex: 1; background: var(--course-grad); }
.hero-body { max-width: 46rem; }
.hero .lead { margin-top: var(--space-5); }
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-6); }

.btn-primary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.btn-ghost { background: transparent; }

/* Real figures, counted up from zero by reveal.js. Nothing rounded, nothing with a plus sign:
   a student who counts the CSS lessons finds 43, and a number they can check is the only kind
   worth printing on a teaching site. */
.stats { margin: var(--space-7) 0; }
.stats ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: var(--space-5); }
.stats li { display: grid; gap: var(--space-1); }
.stats strong { font-size: clamp(2rem, 5vw, 2.8rem); font-weight: 800; letter-spacing: -.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.stats span { color: var(--muted); font-size: .85rem; }

.section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-4); }
.section-head h2 { margin: 0; font-size: 1.5rem; }
.section-more { display: inline-flex; align-items: center; gap: var(--space-2); font-size: .85rem; white-space: nowrap; }

.steps { margin: var(--space-7) 0; }
.step-cards { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--space-4); }
.step-cards li { padding: var(--space-5); display: grid; gap: var(--space-3); align-content: start; }
.step-cards p { margin: 0; font-size: .9rem; }
.step-icon { display: inline-flex; width: 2.25rem; height: 2.25rem; align-items: center; justify-content: center; border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--accent); font-size: 1.1rem; }

.courses-section { margin: var(--space-7) 0; }

/*
 * Course art: the generated cover when there is one, the drawn mark until then. Both are
 * normal states — see Content::cover() — so neither is styled as a placeholder for the other.
 */
.course-art { display: block; overflow: hidden; border-radius: var(--radius); margin-bottom: var(--space-4); }
.course-art.is-cover { aspect-ratio: 16 / 9; background: var(--surface-2); }
.course-art.is-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.course-art.is-mark { aspect-ratio: 16 / 9; display: grid; place-items: center; background: var(--course-grad); }
.course-art.is-mark .course-mark { width: 3rem; height: 3rem; }
/* On the gradient the mark must not also be the course colour, or it disappears into it. */
.course-art.is-mark .course-mark svg { stroke: var(--accent-text); }
.course-art.is-mark .course-mark svg [fill]:not([fill="none"]) { fill: var(--accent-text); }

.course-card .course-chips { flex-wrap: wrap; }

/*
 * REVEAL — an enhancement, never a gate.
 *
 * The offset only applies once reveal.js has added .has-reveal to <html>, which it does only
 * after confirming it can put things back. With JavaScript off, or IntersectionObserver
 * missing, or reduced motion set, none of this applies and every section is simply where it
 * belongs. A decorative animation must never be what stands between a reader and the content.
 */
.has-reveal .reveal { opacity: 0; transform: translateY(12px); }
.has-reveal .reveal.is-in { opacity: 1; transform: none; transition: opacity .5s ease, transform .5s cubic-bezier(.22, .61, .36, 1); }

@media (prefers-reduced-motion: reduce) {
    .has-reveal .reveal { opacity: 1; transform: none; transition: none; }
}

@media (max-width: 40rem) {
    .hero { padding: var(--space-6) 0 var(--space-5); }
    .section-head { flex-direction: column; align-items: flex-start; gap: var(--space-2); }
}

/* ---------------------------------------------------------------------------
 * Overhaul, second pass — corrections made after looking at the rendered page.
 * ------------------------------------------------------------------------- */

/*
 * THE BUG THAT WAS BREAKING EVERY HEADING.
 *
 * .ar carries text-align:right site-wide, which is right for a full Arabic note panel and
 * wrong for an Arabic line sitting under its English twin: the two halves flew to opposite
 * edges and read as two unrelated things rather than one title said twice.
 *
 * dir="rtl" is kept — it is what orders the characters and the punctuation correctly — but the
 * BLOCK aligns with the English above it. Bidi correctness and layout alignment are different
 * questions, and conflating them is what made the lockup look broken.
 */
.lockup-ar,
.bilingual .ar,
.tagline.ar { text-align: left; }

.lockup-ar {
    margin-top: var(--space-1);
    padding-top: 0;
    font-size: .95em;
    line-height: 1.5;
}

/* The rule moves under the pair rather than floating above the Arabic, where it read as a
   stray mark. It is the seam between the two scripts, so it belongs between them. */
.lockup-ar::before {
    position: static;
    display: block;
    width: 1.75rem;
    height: 2px;
    margin-bottom: var(--space-2);
}

.lockup.is-hero .lockup-ar { font-size: clamp(1.2rem, 3vw, 1.9rem); line-height: 1.4; }
.lockup.is-hero .lockup-ar::before { width: 2.5rem; margin-bottom: var(--space-3); }

/* The bilingual pair, tightened: the Arabic is the same sentence, so it sits with its English
   rather than a paragraph away from it. */
.bilingual { display: grid; gap: var(--space-2); }
.bilingual .ar { border-inline-start: 2px solid var(--border); padding-inline-start: var(--space-3); }

/* Hero: two columns on a wide screen, so the right half stops being dead space. */
.hero { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); align-items: center; padding-block: var(--space-7); }
.hero-body { max-width: 34rem; }

.hero-art { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }

.hero-tile {
    display: block;
    overflow: hidden;
    border-radius: var(--radius);
    box-shadow: var(--shadow-2);
    aspect-ratio: 16 / 10;
    /* Alternate rows sit slightly lower, so the block reads as an arrangement rather than a
       grid of thumbnails — the reference opens on one large image; this opens on nine small
       true ones. */
    transform: translateY(calc(var(--tile-offset, 0) * 1px));
}

.hero-tile:nth-child(2), .hero-tile:nth-child(5) { --tile-offset: 18; }
.hero-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (min-width: 62rem) {
    .hero { grid-template-columns: 1.05fr .95fr; }
}

/* Vertical rhythm: the sections were adrift from each other. */
.stats { margin-block: var(--space-6); padding-block: var(--space-5); border-block: 1px solid var(--border); }
.path-strip, .steps, .courses-section { margin-block: var(--space-6); }
.section-head { margin-bottom: var(--space-4); }
.section-head h2 { font-size: 1.35rem; }

/* Cards were leaving a gap where an empty progress track sat. */
.course-card .card-progress { margin-top: var(--space-3); }
.course-card .island-body { display: grid; gap: var(--space-3); align-content: start; }
.course-card .lockup { margin: 0; }
.course-card .tagline { margin: 0; }

/*
 * PHYSICAL properties, deliberately, and only here.
 *
 * These two elements live inside dir="rtl" but must line up with the LTR content above them.
 * Logical properties resolve against the element's own direction, so border-inline-start put
 * the rule on the RIGHT and the lockup's dash floated to the right edge — correct by the
 * spec, wrong for a line whose job is to sit beneath its English twin.
 *
 * Everywhere else in this stylesheet logical properties are right, because the content and its
 * container share a direction. Here they do not, and physical is the honest way to say so.
 */
.lockup-ar::before { margin-left: 0; margin-right: auto; }

.bilingual .ar {
    border-inline-start: 0;
    border-left: 2px solid var(--border);
    padding-inline-start: 0;
    padding-left: var(--space-3);
}

/* The headline was breaking to a second line with one short word on it. */
.lockup.is-hero .lockup-en { max-width: 16ch; }

/* ---------------------------------------------------------------------------
 * Third pass: rhythm. Every section sat on the same flat ground, which is the
 * main reason the page read as a list of blocks rather than as a designed page.
 * ------------------------------------------------------------------------- */

/*
 * A full-bleed ground on a section inside a centred 1100px column.
 *
 * box-shadow + clip-path rather than the usual `margin-inline: calc(50% - 50vw)`: 50vw counts
 * the scrollbar, so on any platform that reserves one the page gains a horizontal scrollbar of
 * its own. The shadow spreads the colour sideways with no layout box at all, and the clip stops
 * it bleeding vertically. Verified: the page still has no horizontal overflow at 390px.
 */
.band {
    position: relative;
    padding-block: var(--space-7);
    clip-path: inset(0 -100vmax);
}

.band-surface { background: var(--surface); box-shadow: 0 0 0 100vmax var(--surface); }

.band-accent {
    background: var(--surface-2);
    box-shadow: 0 0 0 100vmax var(--surface-2);
    border-block: 1px solid var(--border);
}

/* Section headers: an eyebrow that says something factual, then the lockup. */
.section-head { align-items: end; }
.section-head .eyebrow { margin-bottom: var(--space-2); }
.section-head h2 { font-size: clamp(1.5rem, 3vw, 2rem); }

/*
 * The four steps are numbered because they ARE a sequence — read, run, check, hand in, in that
 * order, every lesson. A number here carries information rather than decorating the card, which
 * is the only reason to use one.
 */
.step-cards { counter-reset: step; position: relative; }
.step-cards li { position: relative; padding-top: var(--space-6); }

.step-n {
    position: absolute;
    inset-block-start: calc(var(--space-5) * -1);
    inset-inline-start: var(--space-5);
    width: 2.5rem;
    height: 2.5rem;
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    background: var(--accent);
    color: var(--accent-text);
    font-family: var(--mono);
    font-weight: 700;
    font-size: 1rem;
    box-shadow: var(--shadow-2);
}

.step-icon { width: 2rem; height: 2rem; font-size: 1rem; }
.step-cards li > .lockup { margin-top: var(--space-1); }

/* Course cards: the tagline was running to four lines and pushing every card to a different
   height. Two lines, then it stops — the full text is on the course page. */
.course-card .tagline p { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.course-card .course-art { margin-bottom: 0; }

/* The cover lifts with the card. A still image on a card that moves reads as a sticker on it. */
.course-card .course-art img { transition: transform .4s cubic-bezier(.22, .61, .36, 1); }
.course-card:hover .course-art img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
    .course-card .course-art img,
    .course-card:hover .course-art img { transition: none; transform: none; }
}

.closing { text-align: left; }
.closing-body { max-width: 40rem; }
.closing .hero-actions { margin-top: var(--space-5); }

@media (max-width: 52rem) {
    .step-n { inset-block-start: calc(var(--space-4) * -1); width: 2.1rem; height: 2.1rem; }
}

/* One badge per step card, not two: the number already carries what the icon was saying. */
.step-cards li { padding-top: var(--space-6); align-content: start; }
/* Equal heights, so a title that wraps to two lines does not make its card taller than the
   three beside it. */
.step-cards { align-items: stretch; }
.step-cards li > .lockup .lockup-en { min-height: 2.6em; }

/* ---------------------------------------------------------------------------
 * Fourth pass: the course page. It owned the largest art on the site and was
 * showing a 30px mark in the corner instead.
 * ------------------------------------------------------------------------- */

.course-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
    align-items: center;
    margin-bottom: var(--space-6);
}

.course-hero-text { min-width: 0; }
.course-hero-text .lead { margin-top: var(--space-4); max-width: 46ch; }
.course-hero-text .course-chips { margin-top: var(--space-5); flex-wrap: wrap; }
.course-hero-text .course-requires { margin-top: var(--space-3); }
.course-progress { margin-top: var(--space-5); max-width: 30rem; }

.course-hero-art { border-radius: var(--radius-island); overflow: hidden; box-shadow: var(--shadow-3); }
.course-hero-art.is-cover { aspect-ratio: 16 / 9; }
.course-hero-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.course-hero-art.is-mark { aspect-ratio: 16 / 9; display: grid; place-items: center; background: var(--course-grad); }
.course-hero-art.is-mark .course-mark { width: 4.5rem; height: 4.5rem; }
.course-hero-art.is-mark .course-mark svg { stroke: var(--accent-text); }

@media (min-width: 62rem) {
    .course-hero { grid-template-columns: 1.1fr .9fr; gap: var(--space-7); }
}

/*
 * Depth. Every card was a white rectangle on a grey field with a hairline border, which reads
 * flat next to any modern course site. The elevation tokens already exist and were measured for
 * both themes; the cards simply were not using them.
 */
.course-card, .path-card, .path-chip, .step-cards li, .search-results li {
    box-shadow: var(--shadow-1);
    transition: box-shadow .25s ease, transform .25s cubic-bezier(.22, .61, .36, 1);
}

.course-card:hover, .path-card:hover, .path-chip:hover, .search-results li:hover {
    box-shadow: var(--shadow-3);
    transform: translateY(-3px);
}

/* The course colour is what tells you which card you are on; a bar along the top says it
   without spending any of the card's room on saying it. */
.course-card { position: relative; overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
    .course-card, .path-card, .path-chip, .search-results li,
    .course-card:hover, .path-card:hover, .path-chip:hover, .search-results li:hover {
        transition: none;
        transform: none;
    }
}

/* Path page: the path's own courses are its picture — a sequence illustrated by the sequence. */
.path-hero { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); align-items: center; margin-bottom: var(--space-6); }
.path-hero-text { min-width: 0; }
.path-hero-art { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }

.path-hero-tile {
    display: block;
    border-radius: var(--radius);
    overflow: hidden;
    aspect-ratio: 16 / 10;
    box-shadow: var(--shadow-2);
    /* Stepped, so the block reads as an ordered run rather than a gallery. */
    transform: translateY(calc(var(--i) * 6px));
}

.path-hero-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

.path-step-art { display: block; width: 5.5rem; flex: none; border-radius: var(--radius); overflow: hidden; aspect-ratio: 16 / 9; }
.path-step-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.path-step-art.is-mark { display: grid; place-items: center; background: var(--course-grad); }
.path-step-art.is-mark .course-mark { width: 1.75rem; height: 1.75rem; }
.path-step-art.is-mark .course-mark svg { stroke: var(--accent-text); }

@media (min-width: 62rem) {
    .path-hero { grid-template-columns: 1.05fr .95fr; gap: var(--space-7); }
}

@media (max-width: 40rem) {
    .path-step-art { width: 4rem; }
    .path-hero-tile { transform: none; }
}

/* ---------------------------------------------------------------------------
 * Fifth pass: the type carries the identity, and colour stops being timid.
 * ------------------------------------------------------------------------- */

/*
 * The display face is now the mono, so the tracking that suited Inter is wrong: a monospaced
 * face is already wide, and negative tracking at display size is what keeps it from reading as
 * a code listing rather than a headline.
 */
h1, h2, h3, .lockup-en {
    font-family: var(--display);
    letter-spacing: -.045em;
}

h1 { font-weight: 700; }
.lockup.is-hero .lockup-en { letter-spacing: -.05em; line-height: 1; }
.stats strong { font-family: var(--display); letter-spacing: -.04em; }

/* The Arabic never takes the mono — it has no Arabic glyphs — and never takes negative
   tracking, which breaks Arabic joining. Stated here as well as in the token file because it
   is the rule most likely to be broken by someone adding a heading style later. */
.lockup-ar, .ar, [lang="ar"] { font-family: var(--arabic); letter-spacing: 0; }

/*
 * COLOUR. The nine course colours were doing decorative work — a 4px bar, a chapter number —
 * while every panel stayed white on grey. They are the only thing on this site that says which
 * course you are in without words, so they now carry real area.
 */
.hero { position: relative; }

/*
 * The hero ground, full bleed.
 *
 * A gradient cannot be spread with box-shadow, and an absolutely positioned bleed layer with
 * negative inline offsets WOULD extend the scrollable area and give the page a horizontal
 * scrollbar. So the hero takes a flat tint through the same shadow trick the other bands use —
 * solid, safe, and the spectrum bar above it supplies the colour a gradient would have.
 */
.hero {
    background: var(--accent-soft);
    box-shadow: 0 0 0 100vmax var(--accent-soft);
    clip-path: inset(0 -100vmax);
    padding-block: var(--space-7) var(--space-8);
    margin-bottom: 0;
}

.hero-spectrum { inset-block-start: 0; height: 5px; }

/* A card wears its course's colour along the top edge, so a grid of nine reads as nine
   subjects rather than nine boxes. */
.course-card::before {
    content: '';
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    height: 4px;
    background: var(--course-grad);
    z-index: 1;
}

/* Chips take the course colour on a course card, and stay neutral where there is no course. */
[data-course] .chip { border-color: var(--course-solid); color: var(--course-solid); }

.course-hero-text .chip { font-weight: 600; }

/* The chapter rule and the lockup seam already take --course-grad; the chapter number now
   matches the card bar rather than the generic accent. */
.chapter-title { border-bottom-color: var(--course-solid); }

/* ---------------------------------------------------------------------------
 * Sixth pass: density from real content, and motion that is not decoration.
 * ------------------------------------------------------------------------- */

.levels, .coming { margin-block: var(--space-6); }
.level-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--space-5); }

.level-name {
    font-family: var(--mono);
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--muted);
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-3);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
}

.level-count { margin-inline-start: auto; color: var(--text); font-weight: 700; }
.level-col ul { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-1); }

.level-col a {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius);
    text-decoration: none;
    color: var(--text);
}

.level-col a:hover { background: var(--surface-2); }
.level-col small { margin-inline-start: auto; color: var(--muted); font-family: var(--mono); font-size: .75rem; }
.level-dot { width: .55rem; height: .55rem; border-radius: var(--radius-pill); background: var(--course-grad); flex: none; }

/* "What we have not written yet" is deliberately quiet — it is honest, not a boast, and a
   loud panel would read as advertising the absence. */
.coming-list { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--space-3); }
.coming-list li { padding: var(--space-4); display: grid; gap: var(--space-2); border-style: dashed; }
.coming-path { font-family: var(--mono); font-size: .7rem; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); }
.coming-list strong { font-weight: 600; font-size: .95rem; }
.coming-ar { color: var(--muted); font-size: .88rem; text-align: left; }

.playground-cta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-5);
    padding: var(--space-6);
    margin-block: var(--space-6);
}

.playground-cta h2 { font-size: 1.3rem; margin: 0; }
.playground-cta p { margin: var(--space-3) 0 0; max-width: 44ch; }

/*
 * MOTION.
 *
 * The hero tiles drift, slowly and out of phase, so the opening is not a still photograph.
 * It is the one ambient animation on the site; everything else moves only in response to
 * something the reader did. Ambient motion that cannot be stopped is the kind that makes a
 * page tiring, which is why the whole block is off under prefers-reduced-motion.
 */
@media (prefers-reduced-motion: no-preference) {
    .hero-tile { animation: drift 9s ease-in-out infinite; }
    .hero-tile:nth-child(2) { animation-delay: -1.5s; }
    .hero-tile:nth-child(3) { animation-delay: -3s; }
    .hero-tile:nth-child(4) { animation-delay: -4.5s; }
    .hero-tile:nth-child(5) { animation-delay: -6s; }
    .hero-tile:nth-child(6) { animation-delay: -7.5s; }

    @keyframes drift {
        0%, 100% { transform: translateY(calc(var(--tile-offset, 0) * 1px)); }
        50%      { transform: translateY(calc(var(--tile-offset, 0) * 1px - 8px)); }
    }

    /* Nav links get an underline that grows from the side the language runs from. */
    .nav-link { position: relative; }
    .nav-link::after {
        content: '';
        position: absolute;
        inset-block-end: -.35rem;
        inset-inline-start: 0;
        height: 2px;
        width: 0;
        background: var(--accent);
        transition: width .25s cubic-bezier(.22, .61, .36, 1);
    }
    .nav-link:hover::after { width: 100%; }

    /* Reveals stagger down a grid rather than arriving as one slab. */
    .has-reveal .reveal.is-in > * { animation: rise .5s cubic-bezier(.22, .61, .36, 1) backwards; }
    .has-reveal .reveal.is-in > *:nth-child(2) { animation-delay: .06s; }
    .has-reveal .reveal.is-in > *:nth-child(3) { animation-delay: .12s; }

    @keyframes rise {
        from { opacity: 0; transform: translateY(10px); }
        to   { opacity: 1; transform: none; }
    }
}

/*
 * ARABIC SECOND LINES, all of them this time.
 *
 * An Arabic line beneath its English twin must align with that English. dir="rtl" on a block
 * aligns its text right, so without this the pair flies to opposite edges and reads as two
 * unrelated things rather than one sentence said twice.
 *
 * This was fixed once for the lockup and the bilingual pair, and missed .outcome-ar, .gap-ar
 * and .about-ar — because the fix enumerated the classes someone had noticed rather than the
 * rule. The rule: a class ending in -ar on a dir="rtl" element is a SECOND LINE and aligns
 * left. A wholly Arabic PANEL (.note, .task-brief) is not one of these and right-aligns
 * correctly. tests/bilingual_test.php now walks the views and fails if a new one appears here
 * without being listed.
 */
.lockup-ar,
.outcome-ar,
.gap-ar,
.about-ar,
.coming-ar,
.bilingual .ar,
.tagline.ar { text-align: left; }

/* The gap panel was sitting flush against the steps list above it, so two unrelated blocks
   read as one. It closes a section, so it gets the gap a section boundary needs. */
.path-gaps { margin-top: var(--space-6); }
.path-steps { margin-bottom: 0; }
