/* Self-hosted only — no third-party requests from the signed-in app.
   @import must be the FIRST rule in the file or the browser drops it. */
@import url('fonts.css');

/* `hidden` means "not relevant", and no layout rule should be able to argue
   with it. Any class that sets `display` — `.stack` is flex, and so are most
   of the buttons — beats the user-agent's [hidden] rule on specificity, so an
   element the JS has hidden stays on screen. That bit the creation wizard:
   the final submit button and the whole template form rendered on step one. */
[hidden] { display: none !important; }

:root {
    /* One palette for the whole product. These are docs/DESIGN.md's tokens, so
       the signed-in app and the public site are visibly the same thing.

       Two things this replaces, both on the anti-slop list in CLAUDE.md:
         - a teal accent (#1f6f8b), the single most-flagged AI tell;
         - a warm cream ground (#f2efe7), which with any red accent lands
           straight in AI-design cluster #1.
       Swapping only the accent would have traded one cliche for the other, so
       the ground moved to paper at the same time. */
    --paper: #FBFAF8;
    --bg: #FBFAF8;
    --ink: #14161A;
    --muted: #5A6068;
    /* 5.1:1 on --paper, 5.4:1 on --card. The previous #8B9098 was 3.07:1 and
       failed 1.4.3 wherever it carried real text. */
    --faint: #666B73;

    /* Claret. The one chromatic accent; everything else is paper, ink, hairline. */
    --brand-claret: #7A1F2B;
    --accent: #7A1F2B;
    --accent-strong: #5E1721;

    --card: #FFFFFF;
    --border: #E2E0DB;
    /* WCAG 1.4.11 applies to the boundary that IDENTIFIES a user interface
       component, not to decorative rules. The message box is filled #FFF on a
       #FBFAF8 page — 1.05:1 — so its border is the ONLY thing that says where
       to type. Controls get this (3.18:1 on white); cards keep --border. */
    --control-border: #949086;
    --border-strong: #C9C6BF;

    /* Distinct from the accent on purpose: destructive actions must not read as
       ordinary emphasis now that the accent is itself red. Brighter and more
       orange than claret, and never the only signal on a control. */
    --danger: #B3261E;

    /* Depth is hairlines, not shadows (DESIGN.md §3) — measured as the worst
       paint offender. Kept as a token so anything still asking for it gets
       something almost invisible rather than a drop shadow. */
    --shadow: none;
    /* The only permitted elevation: things that genuinely float above the page
       (dialogs, toasts, popouts). Cards and panels use a hairline instead —
       DESIGN.md §3, where shadows measured as the worst paint offender. */
    --shadow-overlay: 0 8px 28px rgba(20, 22, 26, 0.12);

    --pill-bg: #F2EFEA;
    --field-bg: #FFFFFF;
    --secondary-bg: #F4F3F0;
    --notice-bg: #F4F3F0;
    --badge-bg: #F3E9EA;
    --badge-ink: #7A1F2B;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: 'Geist', 'Segoe UI', system-ui, sans-serif;
    color: var(--ink);
    /* Flat. The radial wash fought the paper ground and gradients are out. */
    background: var(--bg);
}

a {
    color: var(--accent-strong);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

.header {
    /* A band, not floating type. Sticky with a hairline, the same bones as the
       user guide's band, so the product site and its documentation read as one
       surface. Was `padding: 24px 0 8px` with no background — the brand floated
       and the signed-out page had no navigation to hold. */
    position: sticky;
    top: 0;
    z-index: 40;
    background: var(--paper);
    border-bottom: 1px solid var(--rule, #E2E0DB);
    padding: 14px 0;
}

.header .container {
    /* .container carries a 48px bottom margin for page bodies; inside the band
       it would just be 48px of band. */
    margin-bottom: 0;
}

/* ── Signed-out site navigation ─────────────────────────────────────────── */
.topbar-nav {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px 22px;
    font-size: 15px;
}

.topbar-nav a {
    color: var(--ink-soft, #5A6068);
    text-decoration: none;
}

.topbar-nav a:hover,
.topbar-nav a:focus-visible {
    color: var(--ink);
    text-decoration: underline;
    text-underline-offset: 3px;
}

.topbar-nav .topbar-signin {
    color: var(--brand-claret);
    font-weight: 600;
}

.topbar-nav a[aria-current="page"] {
    color: var(--ink);
    text-decoration: underline;
    text-underline-offset: 6px;
    text-decoration-color: var(--brand-claret);
}

@media (max-width: 720px) {
    /* The band goes two rows: brand, then the nav on its own line. */
    .header .topbar { flex-wrap: wrap; gap: 8px 16px; }
    .topbar-nav { gap: 4px 16px; font-size: 14px; }
    .header { position: static; }
}

.container {
    width: min(1100px, 92vw);
    margin: 0 auto 48px;
}

/* Inside an LMS activity there is no header above this, so the card sat hard
   against the top edge of the iframe and read as clipped rather than as the
   top of a page.
   Deliberately small. The header is removed in an embedded view precisely to
   buy vertical space — an 80px bar costs 13% of a 600px Moodle frame — so this
   restores the sense of a top edge without spending that space again. The
   bottom already has the 48px from .container above. */
.is-embedded .container {
    margin-top: 16px;
}

/* Every signed-in page starts below the header, not against it.
   The header is sticky with a hairline under it, so content set flush to the
   top read as if it had scrolled under the bar and been clipped, rather than
   as the top of a page.
   `:not(.is-embedded)` rather than relying on the cascade: inside an LMS the
   header is removed to buy vertical space and the 16px above is deliberately
   smaller — an 80px bar costs 13% of a 600px Moodle frame, and winning that
   back only to spend it here would undo the point of removing it. */
body.is-signed-in:not(.is-embedded) main.container {
    margin-top: 28px;
}

.topbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
}

.brand {
    font-size: 24px;
    font-weight: 600;
    letter-spacing: 0.4px;
}

.role-pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    background: var(--pill-bg);
    padding: 6px 12px;
    border-radius: 999px;
    font-size: 13px;
    color: var(--muted);
}














.card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 18px;
    padding: 20px;
    margin-bottom: 18px;
}

.card h2 {
    margin: 0 0 12px;
    font-size: 20px;
}

.grid {
    display: grid;
    gap: 16px;
}

.grid-2 {
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

label {
    display: block;
    font-size: 13px;
    margin-bottom: 6px;
    color: var(--muted);
}

input[type='text'],
input[type='email'],
input[type='date'],
textarea,
select {
    width: 100%;
    padding: 10px 12px;
    /* --control-border, not --border: a filled field on the paper ground is a
       1.05:1 difference, so this line is the only thing identifying the control
       (WCAG 1.4.11). */
    border: 1px solid var(--control-border);
    border-radius: 10px;
    font-family: inherit;
    /* rem, not px, so the Text size control reaches form fields. 0.875rem is
       exactly the 14px this was, so nothing moves at the default root size —
       it only starts scaling once a student asks for Large or Largest. The
       conformance report recorded the control as partially supported because
       fields and the status line ignored it. */
    font-size: 0.875rem;
    background: var(--field-bg);
    color: var(--ink);
}

input::placeholder,
textarea::placeholder {
    color: var(--muted);
}

textarea {
    min-height: 120px;
    resize: vertical;
}

button,
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 16px;
    border-radius: 10px;
    border: 1px solid transparent;
    background: var(--accent);
    color: #fff;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

/* The browser default ring is a thin dark line, which all but disappears
   against a claret fill — on the student's start page that is the ONLY control
   on the screen. Ink ring, held off the button by 2px of page ground so it
   reads on both the filled and the secondary variant. */
.btn:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}

button:hover,
.btn:hover {
    transform: translateY(-1px);
    /* was a teal glow left over from the old palette */
}

button.secondary,
.btn.secondary {
    background: var(--secondary-bg);
    color: var(--ink);
    border-color: var(--border);
}

.icon-btn {
    width: 36px;
    height: 36px;
    padding: 0;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 0.2s ease, border-color 0.2s ease;
}

.icon-btn svg {
    width: 18px;
    height: 18px;
    fill: currentColor;
}

.icon-btn:hover {
    transform: none;
    box-shadow: none;
}

.icon-btn.danger {
    background: var(--danger);
    border-color: var(--danger);
    color: #fff;
}

.toast {
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translate(-50%, -6px);
    background: var(--card);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 10px 14px;
    box-shadow: var(--shadow-overlay);
    font-size: 13px;
    opacity: 0;
    transform: translate(-50%, -6px);
    transition: opacity 0.2s ease, transform 0.2s ease;
    z-index: 2400;
}

.toast.is-visible {
    opacity: 1;
    transform: translate(-50%, 0);
}

button.danger {
    background: var(--danger);
}

.inline-form {
    display: inline;
}

.notice {
    padding: 10px 12px;
    border-left: 4px solid var(--accent);
    background: var(--notice-bg);
    border-radius: 8px;
    margin-bottom: 12px;
    font-size: 14px;
}

/* A refusal must not look like a save. Colour is not the only signal — the
   word "Not saved" carries it too, because colour alone cannot (WCAG 1.4.1). */
.notice-error {
    border-left-color: #a3341f;
    background: #fdf1ee;
}
.notice-label { color: #a3341f; }
/* The field a complaint is about, marked where the complaint applies. */
.field-error { color: #a3341f; font-size: 13px; margin: 4px 0 0; }

/* Not an error — the form changed something on the teacher's behalf and is
   saying so. It borrows .notice rather than inventing a second left-ruled
   device: CLAUDE.md reserves that rule for ONE semantic role, and .notice
   already holds it. This only tightens the spacing for use inside a field. */
.voice-swap-note {
    margin: 6px 0 0;
    padding: 8px 10px;
    font-size: 13px;
}

.voice-swap-note[hidden] { display: none; }

/* A share link is one unbroken 60-character string. At 320px it pushed the whole
   page sideways — measured, 332px of scroll width in a 320px viewport — which is
   a WCAG 1.4.10 failure on the page, not on the link. `anywhere` rather than
   `break-word` because there is no space in a URL to break at, and it must also
   shrink the element's min-content width so the flex row around it can narrow. */
.share-link {
    overflow-wrap: anywhere;
    min-width: 0;
}
.has-error input,
.has-error textarea,
.has-error select { border-color: #a3341f; }

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

.table th,
.table td {
    text-align: left;
    padding: 10px 8px;
    border-bottom: 1px solid var(--border);
}

.table td:last-child {
    white-space: nowrap;
}

.table .btn {
    margin-right: 6px;
}

.table .btn:last-child {
    margin-right: 0;
}

.table-actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 8px;
    width: 100%;
}

.table th.actions-cell,
.table td.actions-cell {
    text-align: right;
}

.icon-btn.icon-outline svg {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.badge {
    display: inline-block;
    padding: 4px 8px;
    border-radius: 999px;
    background: var(--badge-bg);
    font-size: 12px;
    color: var(--badge-ink);
}

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

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

.auth-card {
    text-align: center;
    padding: 32px 24px;
}

.auth-card h1 {
    margin: 0 0 10px;
    font-size: 28px;
}

.roleplay-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
}

.filter-form {
    overflow-x: auto;
    width: 100%;
}

.filter-row {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    /* `nowrap` with `min-width: 100%` forced five fields onto one line whatever
       the viewport was: at 390px the row ran 274px past the right edge and the
       whole page scrolled sideways to reach a date picker. Wrapping is a reflow
       requirement (WCAG 1.4.10), not a preference. The fields keep a sensible
       floor so they wrap rather than shrinking into unusable slivers. */
    flex-wrap: wrap;
    width: 100%;
}
.filter-field { flex: 1 1 150px; min-width: 0; }
.filter-field input,
.filter-field select { width: 100%; box-sizing: border-box; }

/* A wide table scrolls INSIDE its own box; the page never scrolls sideways.
   The chatbot list is six columns plus a row of actions and cannot fit a
   390px phone — it ran 274px past the right edge, so reaching the delete
   button meant scrolling the whole document, header and all.
   `.table-scroll` was already being written in markup before it existed as a
   rule, which is worse than not having it: the class looked like a guarantee
   and silently was not one. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.table-scroll > .table { min-width: 640px; }

/* Keyboard users must be able to reach a scroll container that has no focusable
   child, so it is focusable — and then it needs a visible focus state like
   anything else that takes focus. */
.table-scroll:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.filter-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.filter-input {
    width: 100%;
}

.filter-input-name {
    width: 200px;
}

.filter-input-unit {
    width: 140px;
}

.filter-input-date {
    width: 160px;
}

.filter-actions {
    display: inline-flex;
    gap: 8px;
    align-items: center;
    align-self: flex-end;
}

.filter-actions .btn,
.filter-actions button {
    height: 38px;
    padding: 0 16px;
}

.topbar-actions {
    display: flex;
    align-items: baseline;
    gap: 18px;
    flex-wrap: wrap;
    justify-content: flex-end;
    font-size: 14px;
}

.topbar-user {
    color: var(--ink);
    font-weight: 500;
}

.topbar-link {
    color: var(--muted);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.topbar-link:hover,
.topbar-link:focus-visible {
    color: var(--accent);
    border-bottom-color: currentColor;
}




.conversation-log {
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--paper);
    padding: 4px 14px;
    min-height: 140px;
    max-height: 320px;
    overflow-y: auto;
    display: grid;
    gap: 10px;
}

.message-row {
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
    font-size: 14px;
    white-space: pre-wrap;
    word-break: break-word;
}

.message-row:last-child {
    border-bottom: none;
}

.thankyou-screen {
    /* No border, no background, no radius: this already sits inside .card, and
       three concentric rounded boxes read as a mistake. Depth here is spacing
       and hairlines — DESIGN.md §3, and the nesting cap in CLAUDE.md. */
    text-align: center;
    padding: 24px 4px 8px;
}

.thankyou-screen h3 {
    margin: 0 0 10px;
    font-size: 22px;
}

.thankyou-screen p {
    margin: 0;
    color: var(--muted);
}

.thankyou-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    justify-content: center;
    max-width: 640px;
    margin: 28px auto 0;
    padding-top: 24px;
    border-top: 1px solid var(--border);
}

.ai-feedback-card {
    /* A section of the page, not a card within a card. One hairline separates
       it; the 4px coloured left-rule and the drop shadow are both on the
       anti-slop list and neither was carrying meaning. */
    margin: 28px auto 0;
    max-width: 640px;
    text-align: left;
    padding: 24px 0 0;
    border-top: 1px solid var(--border);
}

.ai-feedback-heading {
    font-weight: 600;
    font-size: 13px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 14px;
}

.ai-feedback-loading {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--muted);
    font-size: 14px;
    padding: 4px 0;
}

.ai-feedback-loading .chat-loading-spinner {
    width: 20px;
    height: 20px;
    border-width: 3px;
    flex: 0 0 auto;
}

.ai-feedback-loading.is-error {
    color: #b3261e;
}

.ai-feedback-loading.is-error .chat-loading-spinner {
    display: none;
}

.ai-feedback-body {
    font-size: 15px;
    line-height: 1.65;
}

.ai-feedback-body p {
    margin: 0 0 12px;
}

.ai-feedback-body p:last-child,
.ai-feedback-body ul:last-child {
    margin-bottom: 0;
}

.ai-feedback-body ul {
    margin: 0 0 12px;
    padding-left: 22px;
}

.ai-feedback-body li {
    margin: 5px 0;
}

.thankyou-feedback {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: center;
    max-width: 640px;
    margin: 28px auto 0;
    padding-top: 24px;
    border-top: 1px solid var(--border);
}

.thankyou-feedback-label {
    font-weight: 600;
    font-size: 14px;
}

.rating-stars {
    display: flex;
    gap: 6px;
}

/* The group. A fieldset for the grouping semantics, stripped of its chrome. */
.rating-field {
    border: 0;
    margin: 0;
    padding: 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

.rating-field > legend {
    padding: 0;
    float: left;      /* a legend will not honour flex layout in every engine */
    width: 100%;
    text-align: center;
}

.rating-star {
    background: transparent;
    border: none;
    padding: 4px;
    cursor: pointer;
    color: var(--muted);
    display: inline-flex;
}

/* The radio is visually hidden but still focusable, so the focus indicator has
   to be drawn on something visible. Without this the keyboard user selecting a
   rating has no idea which star they are on. */
.rating-star-input:focus-visible + svg {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 3px;
}

.rating-star svg {
    width: 22px;
    height: 22px;
    fill: currentColor;
}

.rating-star.is-selected {
    color: #f2b441;
}

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

.feedback-text {
    width: 100%;
    max-width: 520px;
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 10px 12px;
    background: var(--field-bg);
    color: var(--ink);
    font-family: inherit;
}

.thankyou-status {
    margin-top: 12px;
}

.thankyou-status.is-error {
    color: var(--danger);
}

/* ── The student's start page ─────────────────────────────────────────────────
 *
 * ONE COLUMN, ONE LEFT EDGE. The page used to centre its title, centre the
 * portrait, inset the brief to 90% and auto-margin it, then hang the options
 * panel at the full card width and centre the button — five alignment changes
 * down a single column, with four different left edges (58 / 62 / 75 / 102 at a
 * 920px viewport). Everything below exists to keep that at one.
 *
 * Type roles are docs/DESIGN.md §2, applied here for the first time: the system
 * speaks in Geist, a fact is set in Geist Mono, and the serif is reserved for
 * words a character actually said.
 */
.chat-intro {
    /* Sits inside .card — a second frame around it is the same nesting problem
       the thank-you screen had. */
    padding: 4px 0 8px;
    margin-bottom: 16px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 20px;
    text-align: left;
}

/* Portrait and title on one baseline. Before this the portrait floated centred
   above the title, related to nothing; a student met a face with no name beside
   it and a title that wrapped around it. */
.intro-head {
    display: flex;
    align-items: flex-start;
    gap: 18px;
}

.intro-head-text {
    min-width: 0;
    /* Optically centres a one- or two-line title against the portrait without
       pushing a long one (they run to 90 characters) off the top. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 8px;
    min-height: 96px;
}

.chat-intro h2 {
    margin: 0;
    /* rem, not px: "Largest" in Reading and listening options scales the root
       font-size, and a px heading simply ignores it. */
    font-size: clamp(1.32rem, 2.6vw, 1.7rem);
    line-height: 1.18;
    letter-spacing: -0.02em;
    font-weight: 600;
    text-wrap: balance;
}

.roleplay-title {
    text-align: left;
}

/* §2: mono is a fact. How you answer, how long you have, and whether you get
   another go — none of which appeared until AFTER the student pressed Start. */
.intro-facts {
    margin: 0;
    font-family: 'Geist Mono', ui-monospace, 'SF Mono', monospace;
    font-weight: 500;
    font-size: 0.72rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    /* --muted (6.4:1 on white), not --faint (3.2:1) — this is 11.5px text and
       WCAG AA is a procurement gate, not a polish item. */
    color: var(--muted);
}

/* §2: the serif is a person speaking, and this is the only thing on the page
   anybody says — the character's opening line, shown before the student
   commits. Never use it for a heading; that breaks the device site-wide. */
.intro-spoken {
    margin: 0;
    padding: 16px 20px;
    background: var(--pill-bg);
    border-left: 2px solid var(--border-strong);
    font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
    font-weight: 400;
    font-size: clamp(1.19rem, 2.4vw, 1.5rem);
    line-height: 1.32;
    letter-spacing: -0.01em;
    color: var(--ink);
    max-width: 44ch;
}

/* The brief is the primary content of this page and was set in --muted, which
   left a wall of low-contrast grey as the first thing a student had to read. */
.chat-intro .rich-text {
    width: auto;
    max-width: 68ch;
    margin: 0;
    color: var(--ink);
}

.chat-intro .rich-text p {
    margin: 0 0 12px;
}

.chat-intro .rich-text p:last-child {
    margin-bottom: 0;
}

.intro-actions {
    display: flex;
    align-items: center;
    gap: 14px;
    border-top: 1px solid var(--border);
    padding-top: 20px;
}

.chat-intro .chatbot-avatar {
    width: 96px;
    height: 96px;
    flex: 0 0 auto;
    margin-bottom: 0;
}

.chatbot-avatar {
    width: 140px;
    height: 140px;
    border-radius: 18px;
    object-fit: cover;
    margin-bottom: 12px;
    border: 1px solid var(--border);
}

@media (max-width: 520px) {
    .intro-head {
        gap: 14px;
    }
    .chat-intro .chatbot-avatar {
        width: 72px;
        height: 72px;
    }
    .intro-head-text {
        min-height: 72px;
    }
}

.chatbot-avatar-wrap {
    display: flex;
    justify-content: center;
    margin-bottom: 12px;
}

.chatbot-avatar-small {
    width: 96px;
    height: 96px;
    border-radius: 18px;
    margin-bottom: 0;
}

/* Talking-avatar (3D head) — takes the place of the small static image
 * in-conversation when avatar_mode = 'talking'. */
.chatbot-avatar-wrap--talking {
    margin-bottom: 12px;
}

.talking-avatar-frame {
    position: relative;
    width: 300px;
    max-width: 100%;
    aspect-ratio: 1 / 1.1;
    border-radius: 18px;
    border: 1px solid var(--border);
    overflow: hidden;
    background: var(--secondary-bg);
}

.talking-avatar-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}

.talking-avatar-fallback {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 1;
    transition: opacity 350ms ease;
}

.talking-avatar-fallback.is-visible {
    opacity: 1;
}

.avatar-mode-field {
    display: grid;
    gap: 6px;
}

.rich-text {
    text-align: left;
    margin: 0 auto 16px;
    width: 90%;
    max-width: 90%;
    line-height: 1.6;
}

.rich-text ul,
.rich-text ol {
    margin: 0.5rem 0 0.5rem 1.25rem;
}

.rich-text a {
    color: var(--accent-strong);
}

.avatar-field {
    display: grid;
    gap: 10px;
}

.avatar-preview img {
    width: 120px;
    height: 120px;
    object-fit: cover;
    border-radius: 16px;
    border: 1px solid var(--border);
}

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(31, 30, 27, 0.45);
    display: none;
    align-items: center;
    justify-content: center;
    padding: 20px;
    z-index: 2000;
}

.modal {
    background: var(--card);
    border-radius: 18px;
    border: 1px solid var(--border);
    padding: 20px;
    width: min(560px, 92vw);
    box-shadow: var(--shadow-overlay);
}

.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
}

.modal-header h3 {
    margin: 0;
    font-size: 18px;
}

.modal-close {
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 6px 10px;
    cursor: pointer;
}
.prompt-row {
    display: grid;
    grid-template-columns: 1fr auto auto auto;
    gap: 12px;
    align-items: center;
}

.prompt-row textarea {
    height: 48px;
    min-height: 48px;
    resize: none;
}

.prompt-row .btn,
.prompt-row .mic-button {
    height: 48px;
}

.mic-button {
    width: 48px;
    height: 48px;
    border-radius: 12px;
    border: 2px solid var(--border);
    background: transparent;
    padding: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.mic-button svg {
    width: 100%;
    height: 100%;
    fill: var(--ink);
}

.mic-button.is-recording {
    border-color: var(--danger);
    box-shadow: 0 0 0 6px rgba(184, 59, 47, 0.15);
    animation: pulse-record 1s infinite;
}

.mic-button.is-recording svg {
    fill: var(--danger);
}

@keyframes pulse-record {
    0% {
        box-shadow: 0 0 0 0 rgba(184, 59, 47, 0.3);
    }
    70% {
        box-shadow: 0 0 0 12px rgba(184, 59, 47, 0);
    }
    100% {
        box-shadow: 0 0 0 0 rgba(184, 59, 47, 0);
    }
}

.roleplay-status {
    /* rem for the same reason as the form fields above: this line carries
       "Recording…", "Time is up" and every error the student is given. */
    font-size: 0.875rem;
    color: var(--muted);
}

/* ── Microphone level meter ───────────────────────────────────────────────────
 *
 * Discrete ticks, not a growing glow. CLAUDE.md rules out pulsing indicators
 * and animated status dots, and a bar that swells and brightens is that thing
 * wearing a different shape. Ticks read as an instrument.
 *
 * THE COLOUR IS THE INFORMATION, not decoration:
 *   grey ticks   we can hear this, and we are deliberately not sending it
 *   claret ticks we can hear you and it is going through
 * That is the one distinction a learner cannot otherwise make — the whole
 * reason for showing anything at all — and claret carries it because
 * docs/DESIGN.md reserves the accent for interactive and current state.
 * Height alone carries loudness. */
.mic-meter {
    display: inline-flex;
    align-items: flex-end;
    gap: 2px;
    height: 14px;
    vertical-align: -2px;
}

.mic-meter[hidden] {
    display: none;
}

.mic-seg {
    width: 3px;
    height: 100%;
    border-radius: 1px;
    background: var(--border);
    transition: background-color 90ms linear;
}

.mic-seg.is-lit {
    background: var(--faint);
}

.mic-meter.is-open .mic-seg.is-lit {
    background: var(--accent);
}

.mic-meter-text {
    font-size: 13px;
    color: var(--muted);
}

.mic-meter-text[hidden] {
    display: none;
}

/* The meter's words and the conversation status are two different messages that
   share one line. Without a mark between them they read as one broken sentence:
   "Holding back background noise Live voice connected." Only drawn when the
   words are actually showing. */
.mic-meter-text:not([hidden]) + .roleplay-status::before {
    content: '·';
    margin: 0 7px 0 5px;
    color: var(--rule-strong, #C9C6BF);
}

/* The Test microphone row in the options panel: control, meter, words. */
.mic-check {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 10px 0 0;
}

.reading-options .mic-check .btn {
    padding: 5px 10px;
    font-size: 0.85rem;
}

/* The meter reports a real measurement several times a second, so it is not
   decorative motion and it stays. What goes is the smoothing between states,
   which is the only part that is animation rather than information. */
@media (prefers-reduced-motion: reduce) {
    .mic-seg {
        transition: none;
    }
}

.chat-loading {
    margin-top: 16px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 12px 0;
}

.chat-loading-spinner {
    width: 42px;
    height: 42px;
    border-radius: 50%;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    animation: chat-spin 0.8s linear infinite;
}

.chat-loading-text {
    font-size: 14px;
    color: var(--muted);
}

@keyframes chat-spin {
    to {
        transform: rotate(360deg);
    }
}

.audio-list {
    display: grid;
    gap: 12px;
}

@media (max-width: 720px) {
    .topbar {
        flex-direction: column;
        align-items: flex-start;
    }

    .topbar-actions {
        width: 100%;
        justify-content: flex-start;
    }

    .prompt-row {
        grid-template-columns: 1fr;
    }

    .talking-avatar-frame {
        width: 220px;
    }
}

/* --- Character gallery (teacher create/edit forms) -------------------------
   Replaces the 3D Avatar Studio: a modal with morph sliders, a GLTF export and
   an upload, for a face seen at 300px in a chat window. Six people, one click.
   Markup comes from render_avatar_gallery() in lib/helpers.php.

   Deliberate choices:
   - A roster, not a feature grid: two columns at most, so it reads as a cast
     list inside the form rather than as a marketing tile wall.
   - Selection is signalled three ways — a rule down the card's left edge, a
     tick, and a border/tint shift — so it survives greyscale and low vision.
     That left rule exists for nothing else; it is a state, not decoration.
   - The radio itself stays in the DOM and stays focusable. Arrow keys, form
     submission and `required` are the browser's job, not ours. */
.talking-avatar-picker {
    margin-top: 12px;
    /* Grid items default to min-width:auto, so without this the gallery below
       sizes to its own min-content (two 240px tracks) and punches out of a
       317px form column on a phone. Measured: 490px wide inside a 317px
       parent, page scrolling sideways. */
    min-width: 0;
}

.avatar-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 10px;
    /* width:100% makes the track count resolve against the real column width
       rather than an indefinite one — see the note above. */
    width: 100%;
    min-width: 0;
    max-width: 620px;
}

.avatar-gallery-legend {
    grid-column: 1 / -1;
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    margin: 0 0 2px;
    font-weight: 600;
    font-size: 14px;
}

.avatar-gallery-legend span {
    font-weight: 400;
    font-size: 12px;
    color: var(--muted);
}

.avatar-card {
    position: relative;
    display: grid;
    grid-template-columns: 56px minmax(0, 1fr) 20px;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--field-bg);
    cursor: pointer;
    transition: border-color 120ms ease, background-color 120ms ease;
}

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

/* The selection spine. Zero-width until chosen, so it never reads as trim. */
.avatar-card::before {
    content: '';
    position: absolute;
    left: -1px;
    top: -1px;
    bottom: -1px;
    width: 0;
    border-radius: 14px 0 0 14px;
    background: var(--accent-strong);
    transition: width 120ms ease;
}

.avatar-card.is-selected,
.avatar-card:has(.avatar-card-input:checked) {
    border-color: var(--accent-strong);
    background: color-mix(in srgb, var(--accent) 8%, var(--field-bg));
}

.avatar-card.is-selected::before,
.avatar-card:has(.avatar-card-input:checked)::before {
    width: 5px;
}

/* Off-screen, not display:none — it must still take focus and submit. */
.avatar-card-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

/* Focus ring on the card, driven by the hidden radio. :focus-within is the
   floor; where :has() exists we upgrade to :focus-visible so a mouse click
   does not paint a ring. Visible focus is a procurement gate here, not a
   nicety. */
.avatar-card:focus-within {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

@supports selector(:has(*)) {
    .avatar-card:focus-within {
        outline: none;
    }

    .avatar-card:has(.avatar-card-input:focus-visible) {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
    }
}

.avatar-card-portrait {
    width: 56px;
    height: 56px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--pill-bg);
    object-fit: cover;
    /* Portraits are 1024² with the head high in frame; bias the crop upward
       so a 56px thumbnail shows a face rather than a collar. */
    object-position: 50% 22%;
    display: block;
}

.avatar-card-portrait--legacy {
    display: grid;
    place-items: center;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--muted);
}

.avatar-card-text {
    display: grid;
    gap: 2px;
    min-width: 0;
}

.avatar-card-name {
    font-weight: 600;
    font-size: 15px;
    line-height: 1.2;
}

.avatar-card-role {
    font-size: 12.5px;
    line-height: 1.35;
    color: var(--muted);
}

.avatar-card-tick {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: var(--accent-strong);
    stroke-width: 2.4;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: 0;
    transform: scale(0.6);
    transition: opacity 120ms ease, transform 120ms ease;
}

.avatar-card.is-selected .avatar-card-tick,
.avatar-card:has(.avatar-card-input:checked) .avatar-card-tick {
    opacity: 1;
    transform: none;
}

.avatar-gallery-note {
    max-width: 620px;
    margin-top: 10px;
    font-size: 12px;
}

/* Real empty state: assets/avatars2d/ has nothing talking_avatars() will
   vouch for, so say so instead of offering an option that 404s. */
.avatar-gallery-empty {
    max-width: 620px;
    padding: 12px 14px;
    border: 1px solid var(--danger);
    border-radius: 12px;
    background: color-mix(in srgb, var(--danger) 8%, var(--field-bg));
    font-size: 13px;
}

@media (prefers-reduced-motion: reduce) {
    .avatar-card,
    .avatar-card::before,
    .avatar-card-tick {
        transition: none;
    }
    /* .btn lifts 1px on hover. Small, but it is a transform on every control in
       the product, including the only one on the student's start page. */
    .btn,
    button {
        transition: none;
    }
    .btn:hover,
    button:hover {
        transform: none;
    }
}

/* ── Chatbot form: character gallery as portraits only ───────────────────
   The stock names and occupations are gone from the UI on purpose. The
   teacher casts the character in the scenario, so labelling a portrait
   "Priya, clinical supervisor" actively contradicts a scenario that casts
   her as an 84-year-old resident. The name survives as the accessible name
   and as a title tooltip — an unlabelled radio group would be unusable with
   a screen reader. */
.avatar-card-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.avatar-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 12px;
  /* Six portraits sit as one even row rather than 5 + 1 stranded. */
  max-width: 680px;
}
.avatar-gallery-legend { grid-column: 1 / -1; }
.avatar-card {
  position: relative; display: block; padding: 8px; margin: 0;
  border: 1px solid var(--border, #e2e0db); border-radius: 8px;
  background: transparent; cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.avatar-card:hover { border-color: #b9b5ac; }
.avatar-card:has(:checked) { border-color: #14161a; border-width: 2px; padding: 7px; background: rgba(0,0,0,.03); }
.avatar-card:has(:focus-visible) { outline: 2px solid #14161a; outline-offset: 2px; }
.avatar-card-portrait {
  display: block; width: 100%; height: auto; aspect-ratio: 1 / 1;
  object-fit: cover; border-radius: 6px; background: #eceae5;
}
.avatar-card-input { position: absolute; opacity: 0; pointer-events: none; }

/* ── Chatbot overview ────────────────────────────────────────────────────
   Editing opens on what the chatbot IS, not on twenty-four controls. Same five
   headings as the form and in the same order, so moving between reading and
   editing never asks the teacher to relearn where anything lives.

   Facts sit in a plain definition list on the page's own ground — no card, no
   panel. A summary of a thing does not need a box drawn around every part of
   it, and boxes inside the page's card would be the container-soup the form
   section note above already warns about. */
.overview-facts {
  margin: 0 0 4px;
  display: grid;
  grid-template-columns: minmax(9rem, max-content) minmax(0, 1fr);
  gap: 0;
}
.overview-facts > div { display: contents; }
.overview-facts dt,
.overview-facts dd {
  margin: 0;
  padding: 9px 0;
  border-bottom: 1px solid var(--border, #e2e0db);
}
.overview-facts dt { color: var(--muted, #6b6b76); font-size: 14px; padding-right: 20px; }
.overview-facts dd { font-size: 14px; overflow-wrap: anywhere; }
.overview-facts dd.is-unset { color: var(--muted, #6b6b76); font-style: italic; }
.overview-facts > div:last-child dt,
.overview-facts > div:last-child dd { border-bottom: 0; }

/* Sits in the section heading, which is already a baseline flex row. */
.section-edit { margin-left: auto; font-size: 14px; font-weight: 500; }

.section-actions { display: flex; align-items: center; gap: 16px; margin-top: 18px; }
/* Quiet = a control that reads as text rather than as a button.
   Setting only a colour is all an <a> needs, and that is all this did — but on
   a <button> the `button, .btn` rule above has already applied the claret fill,
   so every quiet BUTTON rendered grey-on-claret at about 1.5:1. Reset the box
   as well as the colour, so the class means the same thing on both elements. */
.btn-quiet {
    font-size: 14px;
    color: var(--muted, #6b6b76);
    background: none;
    border-color: transparent;
    padding: 0;
    border-radius: 0;
}
.btn-quiet:hover { color: var(--ink, #14161a); background: none; }

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

@media (max-width: 560px) {
  /* Stacked: a 9rem label column leaves nothing for the value on a phone. */
  .overview-facts { grid-template-columns: minmax(0, 1fr); }
  .overview-facts dt { padding-bottom: 0; border-bottom: 0; }
  .overview-facts dd { padding-top: 2px; }
}

/* ── Chatbot form: section structure ─────────────────────────────────────
   The form was one flat column of eighteen unrelated fields, which is why it
   read as disjointed: nothing told you that "Realtime voice" belonged with the
   character and "Starter phrase" did not. These headings group it into the
   five decisions a teacher is actually making, and give the eye somewhere to
   rest between them. Hairlines rather than boxes — panels inside a panel is
   the container-soup that made the old page feel cluttered. */
.form-section {
  margin: 40px 0 4px;
  padding-top: 18px;
  border-top: 1px solid var(--border, #e2e0db);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
}
.form-section:first-of-type { margin-top: 24px; }
.form-section > span {
  font-weight: 400;
  font-size: 13px;
  color: var(--muted, #6b6b76);
  letter-spacing: 0;
}

/* ── Chatbot form: Additional settings ───────────────────────────────────
   The two checkboxes used to sit far apart — live feedback buried mid-form
   under the voice controls, template alone after Role prompt — so neither
   read as an option you could take or leave. Together at the end they are
   clearly the optional pair. */
.settings-group { display: grid; gap: 14px; margin-top: 4px; }
.settings-check {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px;
  align-items: start; cursor: pointer; max-width: 68ch;
}
.settings-check input[type="checkbox"] { margin-top: 3px; }
.settings-check strong { display: block; font-weight: 600; font-size: 15px; }
.settings-check span { color: var(--muted, #6b6b76); font-size: 14px; line-height: 1.5; }
.settings-check strong + br { display: none; }

/* ── The two conversation caps ────────────────────────────────────────────
   A number and its "Unlimited" switch sit on one row so the pair reads as a
   single decision. Unlimited greys the number rather than hiding it: a field
   that vanishes makes the teacher wonder what value it will keep. */
.settings-limit { max-width: 68ch; }
.limit-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.limit-row input[type="number"] { width: 10ch; }
.limit-row input[type="number"]:disabled { opacity: 0.45; cursor: not-allowed; }
.limit-unit { font-size: 14px; color: var(--muted, #6b6b76); }
.limit-unlimited {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 14px; color: var(--muted, #6b6b76); cursor: pointer;
}
.settings-limit.is-unlimited .label-row label { color: var(--muted, #6b6b76); }

/* ── In-conversation clock ────────────────────────────────────────────────
   Sits beside the status line, not over the model. Colour is not the only
   signal that time is short — the status line says so in words too, because
   colour alone must not carry meaning (WCAG 1.4.1). */
.roleplay-clock {
  display: inline-block; margin-left: 12px;
  font-variant-numeric: tabular-nums;
  font-size: 13px; color: var(--muted, #6b6b76);
}
.roleplay-clock.is-low { color: #a3341f; font-weight: 600; }

/* A resumed conversation says so, once, above the replayed turns. */
.log-resume-note {
  margin: 0 0 10px; padding-bottom: 8px;
  border-bottom: 1px solid var(--rule, #e2e0db);
  font-size: 13px; color: var(--muted, #6b6b76);
}

/* ── Field info tips ─────────────────────────────────────────────────────
   Built on <details>/<summary> so the whole thing works with JavaScript
   disabled and inherits keyboard and screen-reader behaviour from the
   browser. app.js only adds Escape-to-close, click-outside, and closing any
   other open tip. */
.label-row { display: inline-flex; align-items: center; gap: 6px; }

.info-tip { position: relative; display: inline-block; }
.info-tip-btn {
  display: inline-grid; place-items: center;
  width: 17px; height: 17px; border-radius: 50%;
  /* Optically aligned to the label's cap height rather than its line box.
     A transform, not a margin: the row is a centred flex container, so it
     re-centres the margin box and a negative margin-top has no visible effect. */
  transform: translateY(-3px);
  border: 1px solid var(--border, #cfcbc2); color: var(--muted, #6b6b76);
  font-size: 11px; font-weight: 600; font-style: italic;
  line-height: 1; cursor: pointer; user-select: none;
  list-style: none;                      /* kill the disclosure triangle */
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.info-tip-btn::-webkit-details-marker { display: none; }
.info-tip-btn:hover { border-color: #14161a; color: #14161a; }
.info-tip-btn:focus-visible { outline: 2px solid #14161a; outline-offset: 2px; }
.info-tip[open] > .info-tip-btn { background: #14161a; border-color: #14161a; color: #fff; }

.info-tip-pop {
  position: absolute; z-index: 60; top: calc(100% + 8px); left: -8px;
  width: min(360px, calc(100vw - 48px));
  padding: 14px 16px;
  background: #fff; color: #14161a;
  border: 1px solid var(--border, #d9d5cc); border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0,0,0,.10);   /* a popout needs to lift off the page */
  font-size: 13.5px; line-height: 1.55; font-weight: 400;
  text-align: left; white-space: normal;
}
.info-tip-pop p { margin: 0 0 9px; max-width: none; }
.info-tip-pop p:last-child { margin-bottom: 0; }
.info-tip-title { font-weight: 600; font-size: 14px; }
.info-tip-pop code {
  background: #f1efea; border-radius: 3px; padding: 1px 5px; font-size: .92em;
}
/* Near the right edge, open leftwards so it never leaves the viewport. */
.info-tip-pop.flip-left { left: auto; right: -8px; }

@media (max-width: 560px) {
  .info-tip-pop { position: fixed; left: 16px; right: 16px; width: auto; top: auto; }
}
/* Keep the info icon on the label line inside a settings checkbox — <strong>
   is a block here, so an inline icon after it would drop onto the description. */
.settings-head { display: flex; align-items: center; gap: 6px; }
.settings-head strong { display: inline; }

/* An invisible bridge across the gap between the icon and the panel, so moving
   the pointer into the panel does not cross un-hovered space and dismiss it.
   WCAG 1.4.13 requires hovered content to be hoverable. */
.info-tip-pop::before {
  content: ''; position: absolute; left: 0; right: 0; top: -10px; height: 10px;
}
/* Pinned by a click: the subtle ring says "this one will stay". */
.info-tip.is-pinned > .info-tip-btn { box-shadow: 0 0 0 3px rgba(20,22,26,.12); }

/* --- Voice preview ("Test this voice") ------------------------------------
   Sits under the voice select in both teacher forms and opens a native
   <dialog>, which brings the focus trap, the backdrop and Escape-to-close with
   it — none of that is worth hand-rolling. Sized to match the roleplay avatar
   frame so what the teacher auditions looks like what the student gets. */
.voice-preview {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 8px;
}

.voice-preview-btn {
    padding: 7px 14px;
    font-size: 14px;
}

.voice-preview-status {
    font-size: 13px;
    color: var(--muted, #6b6a65);
}

.voice-preview-status.is-error {
    color: #a3341f;
}

.voice-preview-dialog {
    width: min(420px, 92vw);
    padding: 20px;
    border: 1px solid var(--border);
    border-radius: 18px;
    background: var(--card);
    color: inherit;
    box-shadow: var(--shadow-overlay);
}

.voice-preview-dialog::backdrop {
    background: rgba(31, 30, 27, 0.45);
}

.vp-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    margin-bottom: 14px;
}

.vp-head h3 {
    margin: 0;
    font-size: 18px;
}

.vp-close {
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 6px 10px;
    cursor: pointer;
    color: inherit;
}

.vp-stage {
    position: relative;
    width: 260px;
    max-width: 100%;
    margin: 0 auto;
    aspect-ratio: 1 / 1.1;
    border-radius: 18px;
    border: 2px solid var(--border);
    overflow: hidden;
    background: var(--bg, #f6f4ef);
}

.vp-stage.is-empty {
    /* No portrait to show: collapse rather than frame an empty box. */
    display: none;
}

.vp-fallback,
.vp-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}

.vp-fallback {
    object-fit: cover;
}

.vp-phrase {
    margin: 16px 0 4px;
    text-align: center;
    font-size: 15px;
    line-height: 1.5;
}

.vp-meta {
    margin: 0;
    text-align: center;
    font-size: 13px;
    color: var(--muted, #6b6a65);
}

.vp-actions {
    display: flex;
    justify-content: center;
    margin-top: 16px;
}

@media (max-width: 480px) {
    .vp-stage { width: 200px; }
}

/* --- Wordmark ---------------------------------------------------------------
   Mark + type, not a picture of type: the words stay real text, so they scale,
   copy, translate and read correctly to a screen reader. */

:root { --brand-claret: #7A1F2B; --brand-claret-on-ink: #E0899A; }

.brand {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-size: 24px;
    font-weight: 700;
    letter-spacing: -0.02em;
    /* It is an anchor now — the way home from every page. It should not look
       like body copy that happens to be blue. */
    color: var(--ink);
    text-decoration: none;
}

.brand:hover .brand-word,
.brand:focus-visible .brand-word {
    text-decoration: underline;
    text-underline-offset: 4px;
    text-decoration-thickness: 1px;
}

.brand:focus-visible {
    outline: 2px solid var(--brand-claret);
    outline-offset: 4px;
    border-radius: 3px;
}

.brand-mark {
    width: 1.15em;
    height: 1.15em;
    flex: none;
    background: url('brand/mark.png') center / contain no-repeat;
}

.brand-word {
    font-family: 'Fraunces Wordmark', Georgia, 'Times New Roman', serif;
    font-weight: 700;
    line-height: 1;
    /* Optical: the serif sits slightly high against the mark's flat baseline. */
    transform: translateY(0.02em);
}

.brand-word em {
    font-style: normal;
    /* Deliberately NOT var(--accent): the app's accent is still the old teal,
       which would put the wordmark at odds with its own mark and with the
       landing page. Claret is the brand colour, declared here explicitly. */
    color: var(--brand-claret);
}


/* --- Teacher Mode whiteboard -----------------------------------------------
   Beside the character on a wide screen, below it on a narrow one. A hairline
   frame, no shadow: it is a surface, not a floating card. */
.whiteboard-wrap {
    margin: 0 0 16px;
}

.whiteboard {
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--card);
    padding: 18px 20px;
    min-height: 220px;
    max-height: 60vh;
    overflow: auto;
    text-align: left;
}

/* --- Teaching stage: wide board, slim teacher column -------------------------
   The board fills the row up to a narrow side column: the character at the
   top, a "Whiteboard tools" dock under it, the pair exactly as tall as the
   board. A floating picture-in-picture over the board corner was tried first
   and the teacher kept writing underneath the character; a column costs a
   little width but never hides the working. The tools used to sit in a row
   above the board — moving them into the column gives that height back to the
   board. The board scrolls inside itself and stays pinned to the newest work:
   the page stays still, the last thing written stays in view. */
.teach-stage {
    /* Sized so the whole teaching view — stage, transcript and input — fits a
       laptop viewport without the page scrolling. Scrolling the page is what
       took the board and the teacher off screen in the first place. */
    /* Raised with the figure cap. A 720px-wide 4:3 figure wants ~540px of
       height; 420 clipped it and the board scrolled for every diagram. */
    --stage-h: clamp(260px, 46vh, 560px);
    /* Column width: the character frame is 1/1.1, so this keeps the portrait
       about the size the corner PiP was. */
    --stage-side-w: clamp(104px, 14vh, 148px);
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--stage-side-w);
    gap: 14px;
    align-items: start;
    margin-bottom: 12px;
}

/* Board first visually; the side column stays first in the DOM so the avatar
   renderer bridge and its element ids are untouched. */
.teach-stage > .whiteboard-wrap { order: -1; margin: 0; min-width: 0; }
.teach-stage .whiteboard {
    height: var(--stage-h);
    min-height: 0;
    max-height: none;
    overflow-y: auto;
    /* Vertical only. Sideways scrolling hid the left edge of the teacher's
       working, and nothing on the board is meant to be wider than the panel. */
    overflow-x: hidden;
    overscroll-behavior: contain;
}

.teach-side {
    display: flex;
    flex-direction: column;
    height: var(--stage-h);
    min-width: 0;
}

.teach-stage .chatbot-avatar-wrap { margin: 0; }
.teach-stage .talking-avatar-frame {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1.1;
}
.teach-stage .chatbot-avatar {
    width: 100%;
    height: auto;
    margin-bottom: 0;
    border-radius: 12px;
}

/* The tools dock fills whatever the portrait leaves, so column and board
   always finish level. */
.wb-tools-dock {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
    margin-top: 8px;
}

.wb-tools-label {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
    text-align: center;
    margin-bottom: 6px;
}

.wb-tools-dock .wb-tools {
    flex: 1;
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 6px;
    margin-bottom: 0;
    min-height: 0;
}

.wb-tools-dock .wb-tool {
    width: 100%;
    font-size: 12.5px;
    padding: 6px 8px;
    text-align: center;
}

/* "Rub out mine" is destructive; a gap keeps it away from the mode buttons and
   pushes it down so the column bottoms out level with the board. */
.wb-tools-dock .wb-tool-clear { margin-left: 0; margin-top: auto; }

/* The hint text does not fit the column's fixed height and spilled past the
   board's bottom edge. The buttons' names and titles carry the same guidance,
   so in the dock it is for screen readers only. */
.wb-tools-dock .wb-tools-status {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The transcript is secondary while a lesson is running: it gets a short window
   of its own rather than pushing the stage off screen. */
.teach-stage ~ .conversation-log {
    max-height: 150px;
    min-height: 0;
}

/* Two rows of typing space instead of three: the board earns the difference,
   and the box still grows if the student drags its resize handle. */
.teach-stage ~ #prompt-controls #prompt-input {
    height: 54px;
    min-height: 44px;
}

@media (max-width: 860px) {
    /* The board is the thing you need to see on a phone; the character stays a
       small side presence rather than a shoulder-height portrait. */
    .teach-stage {
        --stage-h: 46vh;
        --stage-side-w: 84px;
        gap: 10px;
    }
    .wb-tools-dock .wb-tool { font-size: 11.5px; padding: 5px 4px; }
}

.whiteboard:empty + .whiteboard-empty { display: block; }
.whiteboard-empty { display: none; color: var(--muted); font-size: 14px; margin: 8px 0 0; text-align: center; }

/* Teacher Mode + realtime voice is a real trap: the board silently does not
   exist. Flag it where the choice is made. */
.behaviour-note-warn {
    color: var(--danger);
}

/* --- Student whiteboard tools ------------------------------------------------
   The student answers ON the board: point at a step, circle what they do not
   follow, write a function. Their ink is blue, distinct from the teacher's
   claret and ink, so who wrote what is never in doubt. */
.wb-tools {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 8px;
    flex-wrap: wrap;
}

.wb-tool {
    font: inherit;
    font-size: 13px;
    padding: 5px 12px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--card);
    color: var(--ink);
    cursor: pointer;
}

.wb-tool:hover { border-color: var(--ink); }

.wb-tool[aria-pressed='true'] {
    background: #1B4D89;
    border-color: #1B4D89;
    color: #fff;
}

.wb-tool-clear { margin-left: 4px; }

.wb-tools-status {
    font-size: 12px;
    color: var(--muted);
    margin-left: 6px;
}

/* The student's layer sits over the teacher's content and never modifies it,
   so a redraw or a clear from the model cannot corrupt their marks. */
.wb-student-layer {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
    overflow: visible;
}

.wb-student-ring { opacity: 0.9; }

.whiteboard[data-tool='marker'] { cursor: crosshair; touch-action: none; }
.whiteboard[data-tool='text'] { cursor: text; }
.whiteboard[data-tool='pointer'] { cursor: pointer; }

.wb-student-text {
    position: absolute;
    z-index: 3;
    font: inherit;
    font-size: 16px;
    color: #1B4D89;
    background: #fff;
    border: 1px solid #1B4D89;
    border-radius: 6px;
    padding: 2px 8px;
    min-width: 160px;
}

/* The teacher's one way through to the conversations behind a launched
   roleplay, shown only to someone entitled to read them (lti_can_view_responses).
   Deliberately a quiet line rather than a button: the subject of this page is
   the roleplay, and two solid buttons above it competed with it. */
.roleplay-owner-link {
    margin: 0 0 0.5rem;
    text-align: right;
    font-size: 13px;
}

.roleplay-owner-link a {
    color: var(--muted);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.roleplay-owner-link a:hover,
.roleplay-owner-link a:focus-visible {
    color: var(--accent);
}

/* Available to screen readers, invisible on screen. A general utility — its
   last caller went when the roleplay's new-tab buttons became an inline link. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Free-trial balance for personal accounts. A single line of text, not a
   banner: it is a fact about the account, not an alert, and it renders on
   every page for the people it applies to. */
.trial-line {
    margin: 0 0 18px;
    padding: 10px 14px;
    border: 1px solid #E2E0DB;
    border-radius: 4px;
    background: #FBFAF8;
    color: #5A6068;
    font-size: 14px;
}
.trial-line strong { color: #14161A; }
/* Underlined, because a link sitting inside a sentence cannot be marked by
   colour alone — claret against this grey is under 3:1 (WCAG 1.4.1). */
.trial-line a { text-decoration: underline; text-underline-offset: 2px; }
/* Both destinations sit together at the end of the sentence and wrap as a
   pair, so on a narrow screen they do not split across lines with the balance
   text between them. */
.trial-links { display: inline-flex; flex-wrap: wrap; gap: 4px 14px; margin-left: 4px; }

/* The banner is one line of text with a ring at the front of it, so the line
   itself becomes a flex row — otherwise the SVG sits on the text baseline and
   drags the line-height around it. */
.trial-line {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.trial-line .chart-donut { flex: none; }
.trial-line .inline-form { display: inline; margin-left: 4px; }

/* The second door on the join card. */
.auth-alt { margin-top: 14px; font-size: 14px; }

/* ── Response detail: the session, legibly ────────────────────────────────────
 *
 * A teacher used to get a name, an email, a date and a wall of transcript. These
 * are the two things that make a tutoring session judgeable in under a minute:
 * a strip of facts, and the whiteboard replayed beside the words it belongs to.
 * The board was captured on every teacher turn from the day the feature shipped
 * and displayed nowhere, so half of every lesson was invisible in review.
 *
 * Hairlines, one accent, no card-in-card nesting — DESIGN.md §3.
 */

.session-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    margin: 0;
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}

.session-facts > div {
    flex: 1 1 8.5rem;
    padding: 10px 14px;
    border-right: 1px solid var(--border);
}

.session-facts > div:last-child { border-right: 0; }

.session-facts dt {
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 2px;
}

.session-facts dd {
    margin: 0;
    font-size: 1.05rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}

.session-facts dd .muted { font-size: 0.8rem; }

/* The tutor produced the student's step. Never colour alone — the label beside it
   already says what the number is, and this adds weight as the second signal. */
.session-facts dd.flagged {
    color: var(--accent);
    font-weight: 600;
}

.turn-list {
    list-style: none;
    margin: 8px 0 0;
    padding: 0;
}

.turn {
    padding: 10px 0 12px;
    border-top: 1px solid var(--border);
}

.turn:first-child { border-top: 0; }

.turn-who {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 4px;
    font-size: 0.74rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
}

/* The tutor's own turns are the ones under review, so they carry the weight;
   the student's stay plain. A left rule would be decoration — this is the one
   semantic role it earns (CLAUDE.md guardrail 9). */
.turn-tutor .turn-who { color: var(--accent); }

.turn-text {
    margin: 0;
    white-space: pre-wrap;
}

.turn-student .turn-text { color: var(--ink); }

/* What the tutor was doing: elicit, hint, repair, assert. This is the difference
   between reading a transcript and seeing a teaching method. */
.turn-step {
    font-size: 0.68rem;
    letter-spacing: 0.03em;
    text-transform: none;
    color: var(--faint);
    border: 1px solid var(--border);
    border-radius: 3px;
    padding: 1px 6px;
}

.turn-board {
    margin-top: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 8px;
    background: var(--card);
    overflow-x: auto;
}

.turn audio {
    margin-top: 8px;
    max-width: 100%;
}

@media (max-width: 640px) {
    .session-facts > div {
        flex-basis: 50%;
        border-bottom: 1px solid var(--border);
    }
}

/* A turn using words the course material never used. A prompt to look, not a
   verdict — so it is a quiet line under the text, not a red banner. Never colour
   alone: the sentence says what it means. */
.turn-flag {
    margin: 6px 0 0;
    font-size: 0.82rem;
    color: var(--accent);
}

/* ── Reading and listening options ────────────────────────────────────────────
 *
 * Scaled from the root so every relative size follows, rather than overriding
 * dozens of rules. High contrast darkens ink and strengthens hairlines; it does
 * not invert, because inverting breaks the whiteboard's named palette — and the
 * board is where the teaching is.
 */
:root[data-text-size="large"]  { font-size: 112.5%; }
:root[data-text-size="xlarge"] { font-size: 131.25%; }

:root[data-contrast="high"] {
    --ink: #000000;
    --muted: #33383F;
    --faint: #4A5058;
    --border: #7A7770;
    --border-strong: #4A4842;
    --card: #FFFFFF;
    --bg: #FFFFFF;
    --paper: #FFFFFF;
    --accent: #5E1721;
    --accent-strong: #431017;
}

/* The panel shares the brief's left edge because both are children of the one
   intro column — it used to sit 40px to the left of the text it belonged to. */
.reading-options {
    margin: 0;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--card);
    text-align: left;
}

.reading-options > summary {
    cursor: pointer;
    padding: 10px 16px;
    font-size: 0.88rem;
    color: var(--muted);
}

.reading-options > summary:hover {
    color: var(--ink);
}

.reading-options > summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.reading-options[open] > summary {
    border-bottom: 1px solid var(--border);
    color: var(--ink);
}

/* Even columns, not a wrapping flex row. Wrapping sized each group to its own
   buttons, which put the three legends on three different x-positions and left
   the voice-speed note hanging out to the right on its own. A grid gives every
   legend and every button row one shared left edge, and collapses to a single
   stacked column in a narrow LMS iframe. */
.reading-options-body {
    padding: 16px;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    align-items: start;
    gap: 20px 24px;
}

.reading-options fieldset {
    border: 0;
    margin: 0;
    padding: 0;
    min-width: 0;
}

.reading-options legend {
    padding: 0;
    font-family: 'Geist Mono', ui-monospace, 'SF Mono', monospace;
    font-weight: 500;
    font-size: 0.7rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 8px;
}

.reading-options .opt-row {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}

.reading-options .opt-row .btn {
    padding: 5px 10px;
    font-size: 0.85rem;
}

/* Belongs to the group above it, so it keeps that group's left edge rather
   than becoming a fourth one. */
.reading-options .opt-note {
    margin: 8px 0 0;
    font-size: 0.8rem;
    line-height: 1.45;
    color: var(--muted);
}

/* Spans every column — it describes the conversation, not one control. */
.reading-options-foot {
    grid-column: 1 / -1;
    margin: 0;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    font-size: 0.8rem;
    line-height: 1.45;
    color: var(--muted);
}

/* The separator above spans every column; the sentence itself keeps a readable
   measure. Putting max-width on the paragraph cut the rule short with it. */
.reading-options-foot > span {
    display: block;
    max-width: 62ch;
}

/* The chosen option, marked by weight and fill as well as colour — aria-pressed
   is the accessible source of truth and this is its visible counterpart. */
.reading-options .opt-row .btn[aria-pressed="true"] {
    border-color: var(--accent);
    color: var(--accent);
    font-weight: 600;
}


/* ── Teaching content panel ───────────────────────────────────────────────────
 *
 * The material a Teacher Mode tutor is allowed to teach from, and the only place
 * a teacher can SEE it. Everything else about a chatbot is visible in a form
 * field; the content was invisible, which meant the one claim the tutor's
 * correctness rests on could only be checked by reading the database.
 */
/* No border-top here: the h3.form-section this panel always contains draws
   one of its own, and the two stacked up as a doubled hairline 28px apart. */
.content-panel {
    margin-top: 12px;
}

.content-docs {
    list-style: none;
    margin: 10px 0 0;
    padding: 0;
}

.content-docs li {
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    font-size: 0.92rem;
}

.content-docs li:last-child { border-bottom: 0; }
.content-docs .muted { margin-left: 8px; font-size: 0.82rem; }

.content-preview {
    margin-top: 12px;
    border: 1px solid var(--border);
    border-radius: 6px;
}

.content-preview > summary {
    cursor: pointer;
    padding: 8px 12px;
    font-size: 0.88rem;
    color: var(--muted);
}

.content-preview > summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.content-preview[open] > summary {
    border-bottom: 1px solid var(--border);
    color: var(--ink);
}

/* Pre-wrapped rather than scrolled sideways: this is prose to be read, and a
   horizontal scrollbar through a page of teaching material is unreadable. */
.content-preview pre {
    margin: 0;
    padding: 12px;
    max-height: 22rem;
    overflow-y: auto;
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 0.85rem;
    line-height: 1.5;
    background: var(--card);
}

.content-upload { margin-top: 16px; }

.content-upload input[type="file"] {
    display: block;
    width: 100%;
    padding: 10px;
    border: 1px dashed var(--border-strong);
    border-radius: 6px;
    background: var(--card);
    font: inherit;
}

.content-upload input[type="file"]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.content-actions {
    display: flex;
    gap: 8px;
    margin-top: 12px;
}

/* ── Creation wizard ────────────────────────────────────────────────────────
   Steps are the form's own sections, revealed one at a time (assets/wizard.js).
   The rail is a map, not a control: a teacher cannot jump to step 5 before
   step 2 is valid, so presenting them as buttons would promise something the
   form will refuse. */
.wiz-rail {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 20px 0 4px;
  padding: 0;
  list-style: none;
  font-size: 13px;
  color: var(--muted, #6b6b76);
  counter-reset: wiz;
}
.wiz-rail-item { counter-increment: wiz; }
.wiz-rail-item::before {
  content: counter(wiz) ". ";
  font-variant-numeric: tabular-nums;
}
.wiz-rail-item.is-done { color: var(--text, #232228); }
.wiz-rail-item[aria-current="step"] { color: var(--text, #232228); font-weight: 600; }

.wiz-step[hidden] { display: none; }
/* The first heading inside a step already spaces itself; a second top border
   from .form-section:first-of-type would double the hairline. */
.wiz-step > h3.form-section { margin-top: 24px; }

.wiz-nav {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 26px;
  padding-top: 18px;
  border-top: 1px solid var(--border, #e2e0db);
}
.wiz-count { margin-left: auto; font-size: 13px; color: var(--muted, #6b6b76); }

/* Route cards. A radio group styled as cards — the input stays real so
   arrow keys, :focus-visible and `required` all behave natively. */
.wiz-routes { border: 0; padding: 0; margin: 0; }
.wiz-routes > legend { padding: 0; }
.wiz-route-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px;
  margin-top: 16px;
}
.wiz-card {
  display: flex;
  gap: 12px;
  padding: 16px;
  border: 1px solid var(--border, #e2e0db);
  border-radius: 10px;
  cursor: pointer;
  background: var(--surface, #fff);
}
.wiz-card:hover { border-color: var(--text, #232228); }
.wiz-card input { margin-top: 3px; flex: 0 0 auto; }
.wiz-card-body { display: flex; flex-direction: column; gap: 5px; }
.wiz-card-body strong { font-size: 15px; }
.wiz-card-body span { font-size: 13px; color: var(--muted, #6b6b76); line-height: 1.45; }
.wiz-card:has(input:checked) { border-color: var(--accent, #8a1c2b); box-shadow: inset 0 0 0 1px var(--accent, #8a1c2b); }
.wiz-card:has(input:focus-visible) { outline: 2px solid var(--accent, #8a1c2b); outline-offset: 2px; }
.wiz-card.is-unavailable { opacity: 0.6; cursor: default; }
.wiz-card.is-unavailable:hover { border-color: var(--border, #e2e0db); }

/* Review reuses the edit overview's fact grid. */
.wiz-review {
  display: grid;
  grid-template-columns: minmax(140px, max-content) minmax(0, 1fr);
  margin: 18px 0 0;
}
.wiz-review > div { display: contents; }
.wiz-review dt,
.wiz-review dd {
  margin: 0;
  padding: 9px 0;
  border-bottom: 1px solid var(--border, #e2e0db);
  font-size: 14px;
}
.wiz-review dt { color: var(--muted, #6b6b76); padding-right: 20px; }
.wiz-review dd { overflow-wrap: anywhere; }
.wiz-review dd.is-unset { color: var(--muted, #6b6b76); font-style: italic; }
.wiz-review > div:last-child dt,
.wiz-review > div:last-child dd { border-bottom: 0; }

@media (max-width: 560px) {
  .wiz-review { grid-template-columns: minmax(0, 1fr); }
  .wiz-review dt { padding-bottom: 0; border-bottom: 0; }
  .wiz-review dd { padding-top: 2px; }
  .wiz-nav { flex-wrap: wrap; }
  .wiz-count { margin-left: 0; width: 100%; }
}

/* Which identity you are on. An LTI-provisioned account carries the display name
   the LMS sent, so without this the header is identical to the local account of
   the same person — and the only clue that they differ is that the dashboard is
   suddenly nearly empty. */
.topbar-via {
  font-weight: 400;
  font-size: 12px;
  color: var(--muted, #6b6b76);
  white-space: nowrap;
}

/* ── the bar across the top of a page inside an LMS activity ────────────────
 *
 * Rendered by views/launch_nav.php. It sits directly beneath the LMS's own tab
 * strip, so it must not compete with it: no filled buttons, no pills, no second
 * accent. Depth is the hairline it hangs from, per DESIGN.md §3.
 *
 * The current item is marked by a claret rule sitting ON that hairline, so the
 * nav reads as tabs cut from the line rather than as chips placed on top of it.
 * Colour is never the only signal — aria-current carries it for assistive
 * technology and the weight change carries it visually. */
.launch-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 0 22px;
    margin: -4px 0 18px;
    border-bottom: 1px solid var(--border);
}

.launch-nav-item {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 0 9px;
    /* Overlap the container's hairline by exactly its own width, so the current
       item's rule replaces that segment instead of doubling it. */
    margin-bottom: -1px;
    border-bottom: 2px solid transparent;
    color: var(--muted);
    font-size: 0.83rem;
    font-weight: 500;
    letter-spacing: 0.01em;
    text-decoration: none;
    white-space: nowrap;
    transition: color 0.15s ease, border-color 0.15s ease;
}

.launch-nav-item:hover,
.launch-nav-item:focus-visible {
    color: var(--ink);
    text-decoration: none;
}

.launch-nav-item.is-current {
    color: var(--accent);
    border-bottom-color: var(--accent);
    font-weight: 600;
}

.launch-nav-item:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 3px;
}

.launch-nav-icon {
    width: 15px;
    height: 15px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Narrow course pages: the bar scrolls, the page does not. */
@media (max-width: 30rem) {
    .launch-nav {
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: 0 18px;
        scrollbar-width: none;
    }
    .launch-nav::-webkit-scrollbar { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .launch-nav-item { transition: none; }
}

/* A page's own action, in the same language as .launch-nav: text and one line
 * icon, no fill. Used where a real action sits beside a heading whose subject
 * is something else — downloading a cohort's evidence matters, but it is not
 * what the responses page is for, and as a filled claret button it was the
 * loudest thing on a page listing students. */
.page-action {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    font-family: inherit;
    font-size: 0.83rem;
    font-weight: 500;
    letter-spacing: 0.01em;
    cursor: pointer;
    transition: color 0.15s ease;
}

.page-action:hover { color: var(--ink); }

.page-action:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 3px;
    color: var(--ink);
}

.page-action-icon {
    width: 15px;
    height: 15px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}

@media (prefers-reduced-motion: reduce) {
    .page-action { transition: none; }
}
