/* ============================================================
   Oak-Ed – Web Awesome base styles
   ============================================================ */

/* --- Fonts: self-hosted, not the Google Fonts CDN ----------------------
   These live here rather than in a per-surface stylesheet because this file
   is the one thing every Oak-Ed surface loads — including the static games
   catalogue, which Caddy serves off disk and which links this file over
   /static/.

   Not the CDN, for two reasons. Privacy: these pages know who is reading
   (the shared header shows the signed-in learner's name) and children load
   them, so they shouldn't leak viewer IPs and referrers to a third party.
   The static catalogue used the CDN back when it was anonymous — bringing it
   into the shared, viewer-aware chrome is what changed that. Reliability:
   Caveat and Patrick Hand are used throughout the games' own gameplay UI, not
   just as decoration, so a blocked or slow third-party request is a visibly
   broken game rather than a cosmetic wobble.

   collectstatic + WhiteNoise give hashing, far-future caching and
   compression, and ManifestStaticFilesStorage rewrites the relative url()
   refs below automatically. Licences: static/fonts/OFL.txt. */
@font-face {
    font-family: 'EB Garamond';
    src: url("../fonts/EBGaramond-Variable.f12bc30ca94a.woff2") format('woff2');
    font-weight: 400 600;  /* one variable file covers the whole range */
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'EB Garamond';
    src: url("../fonts/EBGaramond-Italic-Variable.985f75b87984.woff2") format('woff2');
    font-weight: 400 600;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: 'Patrick Hand';
    src: url("../fonts/PatrickHand-Regular.56bc7c81e5da.woff2") format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Caveat';
    src: url("../fonts/Caveat-Variable.2d9de14f93a2.woff2") format('woff2');
    font-weight: 400 700;
    font-style: normal;
    font-display: swap;
}

html, body {
    min-height: 100%;
    padding: 0;
    margin: 0;
}

/* wa-page banner — logo strip above the header */
.site-banner {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.5rem 1.5rem;
    font-size: 0.8125rem;
}
.site-banner-logo {
    height: 52px;
    width: auto;
}

/* Header layout: brand left, user dropdown right */
.site-header {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-height: 3.25rem;
    padding: 0.5rem 1.5rem;
}
.site-header__top {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 1rem;
}
.site-header__user {
    flex-shrink: 0;
    margin-left: auto;
}
.site-header .brand {
    text-decoration: none;
    font-weight: 700;
    font-size: 1.1rem;
    display: inline-flex;
    align-items: center;
}

/* Sidebar width — only reserve a column on desktop; on mobile the nav is a drawer */
wa-page {
    --menu-width: 200px;
}
wa-page[view="mobile"] {
    --menu-width: 0;
}

/* Main layout */
.page-main {
    padding: 2rem 1.5rem;
    max-width: 1280px;
    margin: 0 auto;
}

/* Page header row */
.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1.5rem;
}
.page-header h1,
.page-header h2 {
    margin: 0;
}

/* Small uppercase heading used at the top of stat / chart cards */
.card-heading {
    font-size: 0.8125rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #6b7280;
    font-weight: 600;
    margin-bottom: 0.75rem;
}

/* Grid layouts */
.grid-3 {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
}
.grid-2-1 {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 1.5rem;
    align-items: start;
}
.centered {
    max-width: 900px;
    margin: 0 auto;
}
.centered-sm {
    max-width: 560px;
    margin: 0 auto;
}

/* Content cards */
.content-card {
    border-radius: 0.75rem;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
}
.content-card-light {
    border-radius: 0.5rem;
    padding: 1rem;
}
.content-card-flush {
    padding: 0;
    overflow: hidden;
}

/* Tables */
.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}
.data-table th {
    text-align: left;
    padding: 0.625rem 0.875rem;
    font-weight: 600;
    font-size: 0.8125rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.data-table td {
    padding: 0.75rem 0.875rem;
    vertical-align: middle;
}
.data-table tbody tr:last-child td {
    border-bottom: none;
}

/* Form fields (for Django-rendered native inputs) */
.form-field {
    margin-bottom: 1rem;
}
.form-field > label {
    display: block;
    font-size: 0.875rem;
    font-weight: 500;
    margin-bottom: 0.3rem;
}
.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="number"],
.form-field input[type="password"],
.form-field input[type="search"],
.form-field input[type="url"],
.form-field select,
.form-field textarea {
    display: block;
    width: 100%;
    padding: 0.5rem 0.75rem;
    border-radius: 0.375rem;
    font-size: 0.875rem;
    box-sizing: border-box;
    font-family: inherit;
    transition: border-color 0.15s, box-shadow 0.15s;
}
.form-field select[multiple] {
    height: auto;
    padding: 0.25rem;
}
.form-field select[multiple] option {
    padding: 0.25rem 0.5rem;
    border-radius: 0.25rem;
}
.form-field input[type="checkbox"],
.filterable-checkbox-list input[type="checkbox"] {
    -webkit-appearance: none;
    appearance: none;
    width: 1.125rem;
    height: 1.125rem;
    border: 2px solid var(--wa-color-neutral-400, #9ca3af);
    border-radius: 0.25rem;
    background: #fff;
    cursor: pointer;
    vertical-align: middle;
    margin-right: 0.375rem;
    flex-shrink: 0;
}
.form-field input[type="checkbox"]:focus-visible,
.filterable-checkbox-list input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--wa-color-brand-600, #3b82f6);
    outline-offset: 2px;
}
.form-field input[type="checkbox"]:checked,
.filterable-checkbox-list input[type="checkbox"]:checked {
    background-color: var(--wa-color-brand-600, #3b82f6);
    border-color: var(--wa-color-brand-600, #3b82f6);
}
.field-error {
    font-size: 0.8125rem;
    margin-top: 0.25rem;
}
.field-help {
    font-size: 0.8125rem;
    margin-top: 0.25rem;
}

/* Input with addon button (e.g. ISBN + Fetch) */
.input-addon {
    display: flex;
    gap: 0.5rem;
    align-items: flex-start;
}
.input-addon > *:first-child {
    flex: 1;
}

/* Badge lists */
.badge-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    align-items: center;
}

/* A <wa-tag> that is the content of a link — the <a> carries the href, so the
   chip navigates as a plain link and needs no click handler. */
a.chip-link {
    text-decoration: none;
}

/* Button rows */
.button-row {
    display: flex;
    gap: 0.5rem;
    align-items: center;
}

/* Breadcrumb */
.breadcrumb {
    display: flex;
    align-items: center;
    gap: 0;
    font-size: 0.875rem;
    list-style: none;
    padding: 0;
    margin: 0 0 1.25rem;
}
.breadcrumb li {
    display: flex;
    align-items: center;
}
.breadcrumb li + li::before {
    content: '/';
    margin: 0 0.5rem;
}
.breadcrumb a {
    text-decoration: none;
}
.breadcrumb a:hover {
    text-decoration: underline;
}
/* The current page is a <span>, not a focusable dummy link: nothing to
   neutralise, just keep it visually distinct from the crumbs you can click. */
.breadcrumb .active {
    color: var(--wa-color-neutral-700, inherit);
}

/* ---------------------------------------------------------------------------
   Portal chrome — the shared header for learner.*, games.* and library.*
   (templates/portal_base.html + partials/portal_sitebar.html).

   Sibling of the .site-header / wa-page chrome above, which is the teacher
   surface. Both live in this file on purpose: one stylesheet is what makes the
   four surfaces one design system. These rules are also the ONLY styling the
   static games catalogue gets — those pages link this file but load no
   WebAwesome kit, so every colour below must keep its literal fallback.
   --------------------------------------------------------------------------- */
.portal-header {
    border-bottom: 1px solid var(--wa-color-neutral-200, #e5e7eb);
    background: var(--wa-color-neutral-0, #fff);
}
.portal-sitebar {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1.5rem;
    max-width: 1280px;
    margin: 0 auto;
}
.portal-sitebar__brand {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    color: inherit;
    font-weight: 700;
}
.portal-sitebar__brand img {
    height: 1.75rem;
    width: auto;
}
.portal-sitebar__wordmark {
    font-size: 1.05rem;
    /* The site name comes from the host label (games / library / learner), so
       it arrives lowercase; small-caps reads as a wordmark rather than a typo. */
    font-variant: small-caps;
    color: var(--wa-color-neutral-600, #4b5563);
}
/* The cross-site links (partials/site_links.html), shared by the portal
   sitebar and the teacher site header — one class family so the two surfaces
   render the row identically. */
.site-links {
    display: flex;
    align-items: center;
    gap: 1rem;
}
.site-links__link {
    color: var(--wa-color-neutral-600, #4b5563);
    text-decoration: none;
    font-size: 0.95rem;
    padding: 0.25rem 0;
    border-bottom: 2px solid transparent;
}
.site-links__link:hover {
    color: var(--wa-color-brand-600, #2563eb);
}
/* The current site renders as a <span>, not an <a> — styled to read as "you
   are here" rather than as a disabled link. */
.site-links__link--current {
    color: var(--wa-color-neutral-900, #111827);
    font-weight: 600;
    border-bottom-color: var(--wa-color-brand-500, #2563eb);
}
.portal-sitebar__viewer {
    margin-left: auto;
    font-size: 0.9rem;
    color: var(--wa-color-neutral-600, #4b5563);
}

/* Per-portal second row: each portal fills {% block localnav %} with its own
   destinations (lessons, trophy case, mail …). Empty on portals that have none,
   and an empty block collapses to nothing rather than an empty bar. */
.portal-localnav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1.25rem;
    padding: 0 1.5rem 0.75rem;
    max-width: 1280px;
    margin: 0 auto;
}
.portal-localnav a {
    color: var(--wa-color-neutral-700, #374151);
    text-decoration: none;
    font-size: 0.95rem;
}
.portal-localnav a:hover {
    color: var(--wa-color-brand-600, #2563eb);
}
.portal-localnav a[aria-current="page"] {
    color: var(--wa-color-neutral-900, #111827);
    font-weight: 600;
}

.portal-main {
    padding: 2rem 1.5rem;
    max-width: 1280px;
    margin: 0 auto;
}

/* Learner-portal tiles. Bigger hit targets and a calmer feel than the dense
   teacher tooling — these were an inline <style> block in the learner base
   until the portals were unified. */
.portal-greeting {
    text-align: center;
    padding: 1rem 1rem 0;
}
.portal-greeting h1 {
    font-size: 2rem;
    margin: 0;
}
.portal-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
    max-width: 36rem;
    margin: 2rem auto;
}
@media (min-width: 640px) {
    .portal-grid {
        grid-template-columns: 1fr 1fr;
    }
}
.portal-tile {
    display: block;
    padding: 1.5rem;
    border-radius: 1rem;
    border: 2px solid var(--wa-color-neutral-200, #e5e7eb);
    text-decoration: none;
    color: inherit;
    transition: transform 120ms ease, border-color 120ms ease;
    min-height: 6rem;
}
.portal-tile:hover,
.portal-tile:focus {
    border-color: var(--wa-color-brand-500, #2563eb);
    transform: translateY(-2px);
}
.portal-tile h2 {
    margin: 0 0 0.25rem;
    font-size: 1.5rem;
}
.portal-tile p {
    margin: 0;
    color: var(--wa-color-neutral-600, #4b5563);
}

@media (max-width: 640px) {
    .portal-sitebar {
        flex-wrap: wrap;
        gap: 0.5rem 1rem;
        padding: 0.75rem 1rem;
    }
    .portal-sitebar__viewer {
        /* Drop the viewer name onto its own line rather than squeezing the
           nav — the nav is the part a child needs to reach. */
        margin-left: 0;
        flex-basis: 100%;
    }
    .portal-localnav {
        padding: 0 1rem 0.75rem;
    }
    .portal-main {
        padding: 1.5rem 1rem;
    }
}

/* Motion is decorative here; children using switch access or with vestibular
   sensitivity should not have to fight it. */
@media (prefers-reduced-motion: reduce) {
    .portal-tile {
        transition: none;
    }
    .portal-tile:hover,
    .portal-tile:focus {
        transform: none;
    }
}

/* Meta info flex row (title + date) */
.meta-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1rem;
}

/* htmx indicator */
.htmx-indicator { opacity: 0; transition: opacity 200ms; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; }

/* Dialogs */
wa-dialog {
    --width: 520px;
}

/* Text helpers */
.text-muted {
    font-size: 0.875rem;
}
.text-sm  { font-size: 0.875rem; }
.text-xs  { font-size: 0.75rem; }
.text-base { font-size: 1rem; }
.text-lg  { font-size: 1.1rem; }
.text-center { text-align: center; }

/* Spacing */
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.mt-5 { margin-top: 1.5rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }

/* Layout */
.w-full  { width: 100%; }
.hidden  { display: none; }
.grid-2  { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.font-mono { font-family: monospace; font-size: 0.85rem; }

/* Empty table / list state */
.empty-state {
    text-align: center;
    padding: 2rem;
}

/* Prose / markdown-rendered content */
.content { line-height: 1.7; }
.content p { margin: 0 0 0.875rem; }
.content p:last-child { margin-bottom: 0; }
.content h1, .content h2, .content h3,
.content h4, .content h5, .content h6 {
    font-weight: 600;
    line-height: 1.3;
    margin: 1.5rem 0 0.5rem;
}
.content h1 { font-size: 1.75rem; }
.content h2 { font-size: 1.4rem; }
.content h3 { font-size: 1.15rem; }
.content ul, .content ol { padding-left: 1.5rem; margin: 0 0 0.875rem; }
.content li { margin-bottom: 0.25rem; }
.content code {
    font-family: monospace;
    font-size: 0.875em;
    padding: 0.1em 0.35em;
    border-radius: 0.25rem;
}
.content pre {
    border-radius: 0.5rem;
    padding: 1rem 1.25rem;
    overflow-x: auto;
    font-size: 0.875rem;
    margin: 0 0 0.875rem;
}
.content blockquote {
    margin: 0 0 0.875rem;
    padding: 0.25rem 0 0.25rem 1rem;
}
.content a {
    text-decoration: none;
}
.content a:hover { text-decoration: underline; }
.content hr {
    border: none;
    margin: 1.25rem 0;
}
.content strong { font-weight: 600; }

/* Subtitle style */
.page-subtitle {
    font-size: 0.9375rem;
    margin-top: 0.25rem;
}

/* ============================================================
   Sidebar navigation (wa-page navigation slot)
   ============================================================ */

.drawer-nav {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    padding: 0.5rem;
}

.nav-link {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.5rem 0.75rem;
    min-height: 44px;
    border-radius: var(--wa-border-radius-medium, 0.375rem);
    color: var(--wa-color-neutral-700, #374151);
    text-decoration: none;
    font-size: 0.875rem;
    font-weight: 500;
    transition: background-color 0.15s, color 0.15s;
}

.nav-link:hover {
    background-color: rgba(0, 0, 0, 0.06);
    color: var(--wa-color-neutral-900, #111);
    text-decoration: none;
}

.nav-link.active {
    background-color: var(--wa-color-brand-100, #dcfce7);
    color: var(--wa-color-brand-700, #15803d);
    font-weight: 600;
}

.nav-link wa-icon {
    font-size: 0.875rem;
    width: 1rem;
    flex-shrink: 0;
}

.data-table tbody tr {
    transition: background-color 0.1s;
}
.data-table tbody tr:hover {
    background-color: var(--wa-color-neutral-90, #f9fafb);
}

/* ============================================================
   Responsive (desktop-first: max-width overrides)
   ============================================================ */

/* Table scroll wrapper — always active so overflow:hidden on the card never clips content */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* 768px — grids collapse, header tightens */
@media (max-width: 768px) {
    .grid-3, .grid-2-1, .grid-2 { grid-template-columns: 1fr; }
    .site-header { padding: 0.375rem 0.75rem; }
    /* The cross-site row can reach four links for a dual-role viewer; let the
       top row wrap rather than overflow, like .portal-sitebar at 640px. */
    .site-header__top { flex-wrap: wrap; }
    .site-banner-logo { height: 40px; }
    .page-main { padding: 1.5rem 1rem; }
}

/* 480px — phone portrait adjustments */
@media (max-width: 480px) {
    .page-main { padding: 1rem 0.75rem; }
    .page-header { flex-direction: column; align-items: flex-start; gap: 0.75rem; }
    wa-dialog { --width: min(520px, calc(100vw - 2rem)); }
}

/* Dashboard overdue alert.
   Intentionally uses explicit hex rather than the --wa-color-danger-* theme
   tokens: that palette's danger shade rendered as a saturated block that was
   too harsh here, so these soft red values were chosen deliberately for a
   calmer alert. */
.dashboard-overdue {
    margin-bottom: 1.5rem;
    border-left: 4px solid #ef4444;
    background: #fef2f2;
}

.dashboard-overdue__heading {
    font-weight: 600;
    font-size: 0.875rem;
    margin-bottom: 0.625rem;
    color: #991b1b;
}

.dashboard-overdue__row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    padding: 0.375rem 0;
    border-bottom: 1px solid #fecaca;
}

.dashboard-overdue__row:last-child {
    border-bottom: none;
}

.dashboard-overdue__learner {
    font-weight: 500;
    font-size: 0.875rem;
}

.dashboard-overdue__link {
    font-size: 0.875rem;
    font-weight: 600;
    color: #b91c1c;
    text-decoration: none;
    white-space: nowrap;
}

/* Filterable checkbox tag selector */
.filterable-checkbox-list {
    border: 1px solid var(--wa-color-neutral-200, #e5e7eb);
    border-radius: 0.375rem;
    padding: 0.5rem;
}

.filterable-checkbox-search {
    width: 100%;
    margin-bottom: 0.5rem;
    padding: 0.25rem 0.5rem;
    border: 1px solid var(--wa-color-neutral-300, #d1d5db);
    border-radius: 0.25rem;
    font-size: 0.875rem;
    box-sizing: border-box;
}

.filterable-checkbox-list .checkbox-scroll {
    max-height: 180px;
    overflow-y: auto;
}

.filterable-checkbox-list ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Internal mail — shared by the teacher pages and the learner portal
   (both surfaces load this stylesheet). Scoped under .mail-*. */
.mail-page {
    max-width: 36rem;
    margin: 1.5rem auto;
    padding: 0 1rem;
}

.mail-card--compose { max-width: 36rem; }
.mail-card--thread { max-width: 42rem; }

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

.mail-thread__item { margin-bottom: 0.75rem; }

.mail-thread__item--mine { text-align: right; }

.mail-thread__bubble {
    display: inline-block;
    max-width: 80%;
    text-align: left;
    padding: 0.5rem 0.75rem;
    border-radius: 0.75rem;
    background: var(--wa-color-neutral-50, #f9fafb);
}

.mail-thread__bubble--mine { background: var(--wa-color-brand-50, #eff6ff); }

.mail-thread__meta { margin-bottom: 0.15rem; }

.mail-thread__body { white-space: pre-wrap; }

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

.mail-conversation-list__item {
    border-bottom: 1px solid var(--wa-color-neutral-100, #f3f4f6);
    padding: 0.6rem 0;
}

.mail-conversation-list__link {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    text-decoration: none;
    color: inherit;
}

.mail-conversation-list__time { margin-left: auto; }

.mail-disclosure { margin-bottom: 1rem; }

/* Fits both placements: below a thread (needs top margin) and at the top
   of a compose form (needs bottom margin). */
.mail-error { margin: 0.75rem 0 1rem; }

.mail-reply-form { margin-top: 1rem; }

.mail-form__label {
    display: block;
    margin-bottom: 0.25rem;
}

.mail-form__field {
    display: block;
    width: 100%;
    box-sizing: border-box;
    margin-bottom: 1rem;
    padding: 0.4rem;
}

.mail-form__field--reply { margin-bottom: 0.5rem; }

/* Learner portal: larger type and touch targets for children. */
.mail-page .mail-form__field {
    padding: 0.5rem;
    font-size: 1.1rem;
}

/* Learner inbox tiles */
.mail-tile-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.mail-tile-list__item { margin-bottom: 0.5rem; }

.mail-tile {
    min-height: auto;
    padding: 1rem;
}

.mail-tile__name { font-size: 1.2rem; }

.mail-compose-tile { margin-bottom: 1rem; }

.mail-empty { text-align: center; }

/* --- DRO dashboard ---
   Wall-display surface: dark ground, big type for TV distance, photo
   background behind a legibility gradient. Everything renders inside the
   single htmx-swapped panel, so these classes carry all the layout.

   Every size here derives from one --dro-scale knob (below) as
   calc(var(--dro-scale) * N) or a plain em, never from the viewport
   directly. That is the whole design: 23 independent clamp()s could — and
   did — disagree with one another, because their vw middle terms were in
   proportion while their rem ends were not. Adding a size means picking an
   N, not writing a new clamp; apps/dro/tests/test_wall_layout.py fails the
   build on a font-size that measures the viewport itself. Each N below is
   the old clamp's vw term over 2.2, so the proportions between the parts
   are the ones that were already written here; only the base they hang off
   is recalibrated, at the knob. */

.dro-display-body {
    margin: 0;
    min-height: 100vh;
    /* The one knob the whole wall scales from — set here, on <body>, rather
       than on .dro-display, so the "Enable alerts" chip scales with the
       frame: it is rendered in door_extras after </main>, a sibling of the
       panel, and would inherit nothing set inside it.

       min() so the frame fits by whichever dimension is tighter: 2.67vh is
       the 16:9 equivalent of 1.5vw, so a 16:9 screen is governed by width
       and anything shorter — a 16:10 laptop, a half-height browser window —
       now gets a say it did not have when every size was width-driven.

       No ceiling, deliberately. A wall display is proportional by design: a
       bigger screen is a further-away screen, and a rem ceiling is anchored
       to the viewer's own font size and OS scaling rather than to the wall.
       Ceilings are what broke this — on a laptop every one of them bound at
       once, so a 1366px screen rendered a 1920px TV's type into two thirds
       of its height. The floor stays: 1.5vw on a 360px phone is 5px, and
       the session door is opened on phones.

       1.5vw, not the 2.2vw the old middle terms were written in, because
       that middle term is not what any real screen rendered. The 1.8rem
       ceiling bound at every width above ~1310px, so every desktop and
       laptop alike drew this frame at 28.8px — and 28.8px is 1.5vw of the
       1920px screen the wall was tuned on. Carrying 2.2vw over would have
       made the frame 40% bigger than anyone has ever seen it.

       --dro-zoom is the viewer's own multiplier: the `+`/`-` keys (see
       [data-dro-views] in static/js/oak-ed.js) step it and set it on this
       <body>, and each browser remembers its own. It multiplies the knob
       rather than replacing it so a zoomed wall still follows the screen it
       is on. The fallback of 1 is what a page that has never been zoomed
       renders at, which is exactly what it rendered before the keys existed. */
    --dro-scale: calc(var(--dro-zoom, 1) * max(1rem, min(1.5vw, 2.67vh)));
    font-size: var(--dro-scale);
    background: var(--wa-color-neutral-950, #111827);
    color: var(--wa-color-neutral-0, #f9fafb);
}

/* Only the TV door freezes the viewport, and only at TV-ish widths. body
   { overflow: hidden } propagates to the viewport, and the session door is one
   tap from the learner portal on a phone — there, a daily view with a dozen
   events has to scroll. The wall link is opened on phones too (it is the one
   URL that needs no login), so below tablet width it scrolls like the session
   door; apps/dro/tests/test_wall_layout.py pins both halves. */
@media (min-width: 48rem) {
    .dro-display-body--wall {
        overflow: hidden;
    }
}

/* The photo arrives as --dro-photo, set inline by partials/panel.html (unset
   means the plain dark ground). One URL, two layers: a blurred cover-scaled
   copy fills the screen, and the sharp photo sits on it scaled to fit, so a
   portrait shot on a landscape TV is shown whole instead of cropped. The
   original file is never resized — scaling is the browser's, at paint time.
   No transition: each htmx poll swaps this node out of band, so a
   background-image transition here never actually ran. */
.dro-background {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
}

/* inset is negative so the blur samples past the viewport edge instead of
   fading to transparent at the rim; overflow: hidden above clips the bleed. */
.dro-background::before {
    content: '';
    position: absolute;
    inset: -3rem;
    background-image: var(--dro-photo, none);
    background-size: cover;
    background-position: center;
    filter: blur(1.5rem);
}

/* Sharp photo under the legibility gradient (text sits over family photos of
   unknown brightness). Layers list top-first, so the gradient darkens both the
   photo and the blurred bars. The `none` fallback matters: an unset custom
   property would invalidate the whole declaration and drop the gradient too. */
.dro-background::after {
    content: '';
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(17, 24, 39, 0.55), rgba(17, 24, 39, 0.75)),
        var(--dro-photo, none);
    background-size: auto, contain;
    background-repeat: no-repeat;
    background-position: center;
}

.dro-display {
    position: relative;
    z-index: 1;
    min-height: 100vh;
    box-sizing: border-box;
    padding: calc(var(--dro-scale) * 1.82);
}

.dro-topstrip {
    display: flex;
    align-items: baseline;
    gap: calc(var(--dro-scale) * 1.36);
    flex-wrap: wrap;
    margin-bottom: calc(var(--dro-scale) * 0.91);
}

.dro-clock {
    font-family: 'EB Garamond', Georgia, serif;
    font-size: calc(var(--dro-scale) * 3.64);
    line-height: 1;
    font-weight: 600;
}

.dro-date,
.dro-weather {
    font-size: calc(var(--dro-scale) * 1.36);
    color: var(--wa-color-neutral-200, #e5e7eb);
}

/* Weather lookahead: one row of the hours remaining today, shown on the
   daily view only (the weekly view puts each day's weather in its own
   column header). Overflow hides rather than wraps — a wall display must
   never scroll, and the strip is already capped server-side (HOURLY_MAX). */
.dro-forecast {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: calc(var(--dro-scale) * 1.36);
    margin-bottom: calc(var(--dro-scale) * 0.91);
    font-size: calc(var(--dro-scale) * 0.73);
    color: var(--wa-color-neutral-200, #e5e7eb);
}

.dro-forecast__hourly {
    display: flex;
    gap: calc(var(--dro-scale) * 0.91);
    /* A flex item refuses to shrink below its nowrap content unless told to;
       without this the strip widened the page past a phone screen instead of
       clipping (TV) or scrolling inside itself (phone, below). */
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
}

.dro-forecast__hour {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15em;
}

.dro-forecast__label {
    font-size: 0.8em;
    color: var(--wa-color-neutral-400, #9ca3af);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.dro-forecast__uv {
    font-size: 0.75em;
    color: var(--wa-color-warning-300, #fcd34d);
}

/* UV chips take the WHO scale's colour by band (apps/dro/weather.py:UV_BANDS
   sets the slug; the label rides only the current reading, the rest of the
   scale is carried by colour alone). Lifted a tint or two from the textbook
   green/yellow/orange/red/violet so each reads on the dark ground at TV
   distance; the fallback above stays for a chip whose band is unknown. */
.dro-uv { white-space: nowrap; }
.dro-uv--low { color: var(--wa-color-success-300, #86efac); }
.dro-uv--moderate { color: var(--wa-color-warning-300, #fde047); }
.dro-uv--high { color: #fdba74; }
.dro-uv--very-high { color: var(--wa-color-danger-300, #fca5a5); }
.dro-uv--extreme { color: #d8b4fe; }

/* Rain chance under the UV chip on hourly chips, beside it on weekly headers:
   .dro-forecast__hour stacks its chips in a column, the weekly header row does
   not. Both surfaces want the same chip either way — view_weekly.html already
   reuses .dro-forecast__uv the same way. The text is weather.py:rain_label —
   chance, amount in mm, or both — and the chip only renders when a provider
   carries at least one of the two series. */
.dro-forecast__rain {
    font-size: 0.75em;
    color: var(--wa-color-brand-300, #93c5fd);
}

/* The current UV band's advice under the topstrip, in the band's own colour
   (.dro-uv--* above). Clipped rather than wrapped, like the headline below,
   even though this line is instruction: a two-line clamp would push the
   whole frame down and up again as the band changes through the day, and
   the longest band's sentence (~90 characters) fits one line at the wall's
   own width. It only clips on a viewport this page was never sized for. */
.dro-uv-advice {
    margin: 0 0 calc(var(--dro-scale) * 0.45);
    font-size: calc(var(--dro-scale) * 0.64);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* AccuWeather's one-line written summary, daily view only. Headlines can
   be long and a wall display must never reflow: clip, never wrap. */
.dro-forecast__headline {
    margin: 0 0 calc(var(--dro-scale) * 0.45);
    font-size: calc(var(--dro-scale) * 0.64);
    color: var(--wa-color-neutral-400, #9ca3af);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* High-contrast, deliberately static (animation: none — no flashing on a
   screen a child watches all day). */
.dro-banner {
    animation: none;
    display: inline-block;
    padding: 0.5em 1.2em;
    border-radius: 999px;
    margin-bottom: calc(var(--dro-scale) * 0.91);
    background: var(--wa-color-brand-600, #2563eb);
    color: var(--wa-color-neutral-0, #fff);
    font-size: calc(var(--dro-scale) * 1.36);
}

/* Phone-width — the session door — is full width: a block-level list already
   fills its container, so the hug-to-content width lives in the tablet
   breakpoint below, not here. */
.dro-agenda {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.3em;
}

/* The daily view's rows are its repeating content block, and until now they
   carried no font-size at all — so they inherited the frame base and were
   the largest body text on the page, larger than the date and weather
   beside them and a third larger than the same content in the weekly view
   (.dro-week__event, 0.75em). A row also spent more height on whitespace
   than on text: 0.8em of padding and a 0.6em gap against ~1.3em of line.

   0.85em with the spacing tightened puts the row back below the topstrip
   it sits under while giving up little legibility at distance: measured on
   the wall, the row pitch drops from 84px to 50px — 40% — for text only
   15% smaller, so ten events now fit a screen height where seven did.

   line-height is the surprise and the biggest single win. Nothing ever set
   it, and `normal` for this face is about 1.6 — 38px of the old 84px pitch
   was leading, more than the padding and the gap put together. 1.25 is
   ample for rows that are one line of text against a feed stripe, and it
   costs nothing in legibility the way shrinking the type does. Set it here
   rather than on .dro-agenda so a wrapped long title keeps it too.

   Not the weekly's 0.75em:
   this is the focused view, and its rows should still read as the largest
   thing in the frame after the clock. .dro-agenda__time needs no change —
   its 6.6em floor is local, so the label column narrows with the text and
   still fits "10:00–11:30". */
.dro-agenda__item {
    display: flex;
    align-items: baseline;
    font-size: 0.85em;
    line-height: 1.25;
    gap: 0.8em;
    padding: 0.25em 0.8em;
    border-left: 0.25em solid var(--wa-color-brand-600, #2563eb);
    background: rgba(17, 24, 39, 0.55);
    border-radius: 0 0.4em 0.4em 0;
}

.dro-agenda__item--imminent { font-weight: 700; }

/* The label sits at its own width on the 360px phone session door, where
   this non-wrapping flex row (.dro-agenda__item) has no title room to
   spare: the floor only keeps "HH:MM" rows aligned, and flex-shrink: 0
   stops a long title squeezing a wider label ("from 22:00") into the gap,
   which nowrap would otherwise paint over the title. From 640px up (tablet
   and the TV door) the floor widens to fit "10:00–11:30" so the title
   column lines up across rows. */
.dro-agenda__time {
    font-variant-numeric: tabular-nums;
    color: var(--wa-color-neutral-300, #d1d5db);
    min-width: 3.2em;
    flex-shrink: 0;
    white-space: nowrap;
}

@media (min-width: 640px) {
    /* Hug the widest row: grid items stretch to the track, so every row ends
       up as wide as the longest title rather than the whole screen (on the TV
       that was a ~1700px bar around five words). Capped at a third of the TV,
       but never narrower than the 6.6em time column plus a few words of
       title — a literal third of a 768px tablet is ~256px.

       NOTE: the 28rem floor stays rem while the rest of this block moved to
       --dro-scale. It is a minimum usable width, not a type size, and it
       only ever binds below ~1344px, where a third of the screen is the
       narrower of the two — exactly the widths at which the scale is small
       and rem is a fair anchor. Above that the percentage governs and the
       floor is dead, so there is no large screen for it to be wrong on. In
       em it would have been worse, not better: the em basis grows with the
       viewport, so the floor would overtake the percentage on a laptop and
       widen the agenda there, which is the opposite of what this change is
       for. */
    .dro-agenda {
        width: fit-content;
        max-width: max(33.333%, 28rem);
    }

    .dro-agenda__time {
        min-width: 6.6em;
    }
}

/* Progress against the clock. An ended row fades as a whole — feed stripe
   included — so "done" reads at TV distance; a running row is bold on a
   lifted neutral ground (not brand blue: it must not fight the feed colour),
   which keeps it distinct from --imminent's bold-only "soon". */
.dro-agenda__item--ended,
.dro-week__event--ended {
    opacity: 0.45;
}

.dro-agenda__item--running,
.dro-week__event--running {
    font-weight: 700;
    background: rgba(107, 114, 128, 0.45);
}

.dro-agenda__where {
    color: var(--wa-color-neutral-400, #9ca3af);
    font-size: 0.8em;
}

/* The daily view's weather chart (templates/dro/partials/forecast_chart.html).
   Beside the agenda when the panel has one; without it .dro-view--daily is
   the single column it always was. */
.dro-view--charted {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    gap: calc(var(--dro-scale) * 1.82);
    align-items: start;
}

/* One grid for the whole chart: a title column, then a column per hour. The
   canvas panel spans the hour columns and every row puts a cell in each, so
   the curve, the bars and the numbers share their centres — see the geometry
   contract in apps/dro/chart.py. The palette lives here, not in the script:
   droDrawChart reads these three properties. */
.dro-chart {
    --dro-chart-temp: var(--wa-color-neutral-0, #f9fafb);
    --dro-chart-feels: rgba(249, 250, 251, 0.5);
    --dro-chart-rain: rgba(147, 197, 253, 0.32);
    display: grid;
    grid-template-columns: max-content repeat(var(--dro-chart-columns, 12), minmax(0, 1fr));
    row-gap: calc(var(--dro-scale) * 0.55);
    align-items: center;
    min-width: 0;
    font-size: calc(var(--dro-scale) * 0.64);
    color: var(--wa-color-neutral-400, #9ca3af);
}

.dro-chart__row {
    display: contents;
}

.dro-chart__title {
    grid-column: 1;
    padding-right: 1em;
    font-weight: 600;
    color: var(--wa-color-neutral-0, #f9fafb);
}

.dro-chart__unit {
    display: block;
    font-size: 0.8em;
    font-weight: 400;
    color: var(--wa-color-neutral-400, #9ca3af);
}

/* Height is the one free choice here; width is the grid's. The canvas is
   absolutely positioned so Chart.js's responsive resize reads the wrapper,
   not its own previous size. */
.dro-chart__panel {
    grid-column: 2 / -1;
    position: relative;
    height: calc(var(--dro-scale) * 9);
    border-bottom: 1px solid var(--wa-color-neutral-700, #374151);
}

.dro-chart__panel canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

/* Sat on its point by left/top, then lifted clear of the line. */
.dro-chart__temp-label {
    position: absolute;
    transform: translate(-50%, -135%);
    font-weight: 600;
    color: var(--wa-color-neutral-0, #f9fafb);
}

.dro-chart__bar-label {
    position: absolute;
    bottom: 0.2em;
    transform: translateX(-50%);
    font-size: 0.85em;
    color: #93c5fd;
}

.dro-chart__cell {
    text-align: center;
    line-height: 1.15;
}

.dro-chart__cell--emphasis {
    font-weight: 700;
    color: var(--wa-color-neutral-0, #f9fafb);
}

.dro-chart__detail {
    display: block;
    font-size: 0.75em;
}

/* The band colour arrives as `color` from the shared .dro-uv--* classes; the
   cell turns it into a background so the chart can never disagree with the
   chips in the strip. The value inside is set dark again. */
.dro-chart__cell--uv {
    margin: 0 0.12em;
    padding: 0.3em 0;
    border-radius: 0.3em;
    background: currentColor;
}

.dro-chart__cell--uv .dro-chart__value {
    font-weight: 700;
    color: #111827;
}

.dro-chart__key {
    grid-column: 2 / -1;
    display: flex;
    flex-wrap: wrap;
    gap: 0.2em 1.1em;
    margin: 0;
    font-size: 0.85em;
}

.dro-chart__key-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
}

.dro-chart__swatch {
    width: 0.8em;
    height: 0.8em;
    border-radius: 0.15em;
    background: currentColor;
}

.dro-chart__key-text {
    color: var(--wa-color-neutral-400, #9ca3af);
}

/* "Hide the weather" means all of it: the chart goes, and the agenda gets
   its column back. */
.dro-hide-weather .dro-view--charted {
    grid-template-columns: minmax(0, 1fr);
}

/* Per-screen chart switches (the 'o' dialog's Weather section). Same
   mechanism as the dro-hide-* classes: on the persistent <main>, reaching
   into the swapped frame by descendant selector, so a poll cannot undo them.
   The feels-like line and the rain bars live on the canvas, where CSS cannot
   reach — droChartDatasets in static/js/oak-ed.js leaves those out. */
.dro-hide-chart .dro-chart,
.dro-hide-chart-rain .dro-chart__row--rain,
.dro-hide-chart-rain .dro-chart__bar-label,
.dro-hide-chart-uv .dro-chart__row--uv,
.dro-hide-chart-wind .dro-chart__row--wind,
.dro-hide-chart-humidity .dro-chart__row--humidity {
    display: none;
}

.dro-hide-chart .dro-view--charted {
    grid-template-columns: minmax(0, 1fr);
}

.dro-week {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 0.25em;
}

.dro-week__day {
    background: rgba(17, 24, 39, 0.55);
    border-radius: 0.4em;
    padding: 0.5em;
    min-height: 8em;
}

.dro-week__day--today {
    outline: 2px solid var(--wa-color-brand-600, #2563eb);
}

.dro-week__day h3 {
    margin: 0 0 0.4em;
    font-size: 0.9em;
    color: var(--wa-color-neutral-300, #d1d5db);
}

/* The day's own forecast, under its column heading. nowrap because the
   seven columns are equal fractions — a wrapped range would push one
   column's events out of line with its neighbours. */
.dro-week__weather {
    margin: 0 0 0.4em;
    font-size: 0.75em;
    color: var(--wa-color-neutral-300, #d1d5db);
}

/* nowrap on the spans, not the paragraph: the temperature range must never
   split internally, but the UV chunk may drop to its own line — nowrap on
   the whole <p> would push it past the 1fr column edge on narrow screens. */
.dro-week__weather > span {
    white-space: nowrap;
}

/* One chip per elevated pollen/air reading under a weekly column heading —
   the same warning tint as .dro-uv--high, since both mean "plan around it". */
.dro-week__pollen {
    color: #fdba74;
}

.dro-week__event {
    margin: 0 0 0.3em;
    padding: 0.15em 0.4em;
    border-left: 0.18em solid var(--wa-color-brand-600, #2563eb);
    border-radius: 0 0.3em 0.3em 0;
    font-size: 0.75em;
}

/* Corner cards: of-the-day and the library notice share one family. */
.dro-of-the-day,
.dro-library {
    position: fixed;
    right: calc(var(--dro-scale) * 1.36);
    bottom: calc(var(--dro-scale) * 1.36);
    z-index: 1;
    max-width: min(19em, 40vw);
    padding: 1em 1.2em;
    border-radius: 0.6em;
    background: rgba(17, 24, 39, 0.75);
    font-size: calc(var(--dro-scale) * 0.73);
}

.dro-library { right: auto; left: calc(var(--dro-scale) * 1.36); }

.dro-of-the-day img,
.dro-library img {
    max-width: 100%;
    max-height: 30vh;
    border-radius: 0.4em;
}

.dro-of-the-day__title { margin: 0.4em 0 0; font-weight: 600; }
.dro-of-the-day__body { margin: 0.3em 0 0; }

.dro-of-the-day__attribution,
.dro-library__review {
    margin: 0.3em 0 0;
    color: var(--wa-color-neutral-300, #d1d5db);
    font-size: 0.85em;
}

.dro-library__title { margin: 0.4em 0 0; }

/* 't' turns the wall into a photo frame: everything in the frame but the
   topstrip disappears, leaving the background photo alone. 'c' governs the
   clock and date within that stripped-down strip — off by default the
   moment text goes off (a bare photo), 'c' toggles them back on as a
   screen-saver clock. Both classes sit on .dro-display's persistent <main>
   (static/js/oak-ed.js), never on anything an htmx poll swaps, so a poll
   landing mid photo-frame cannot silently undo either toggle. The
   background photo itself is untouched — it lives in the sibling
   #dro-background node these selectors never reach. */
.dro-text-off [data-dro-current-view]>:not(.dro-topstrip) {
    display: none;
}

.dro-text-off .dro-topstrip>:not(.dro-clock):not(.dro-date) {
    display: none;
}

.dro-clock-off .dro-clock,
.dro-clock-off .dro-date {
    display: none;
}

/* Per-screen hide switches from the settings dialog: the schoolroom TV may
   not want the weather the kitchen screen does. Classes on the persistent
   <main>, like the two above. The server still renders what these hide —
   the same trade dro-text-off makes — because the preference lives in the
   browser and the fragment must stay cacheable per household, not per
   screen. */
.dro-hide-of-the-day .dro-of-the-day,
.dro-hide-library .dro-library,
.dro-hide-weather .dro-weather,
.dro-hide-weather .dro-uv-advice,
.dro-hide-weather .dro-forecast__headline,
.dro-hide-weather .dro-forecast,
.dro-hide-weather .dro-chart,
/* The weekly view's own per-day line, which no daily-view selector above
   reaches — it carries the UV, rain and pollen chips as children, so this
   one selector takes the whole column's weather with it. */
.dro-hide-weather .dro-week__weather {
    display: none;
}

/* The session door's "Enable alerts" chip (static/js/oak-ed.js hides it once
   there is nothing left for it to arm). Fixed and above .dro-display's
   z-index: 1 so it sits over the wall, not inside its padding; a plain
   <button>, not wa-button — this fires before the WebAwesome kit script has
   necessarily finished registering it.

   Top corner, not bottom: .dro-of-the-day and .dro-library own both bottom
   corners from the tablet breakpoint up, and an opaque pill painting above
   them covers whichever it lands on. Nothing else can reach the band above
   .dro-display's own padding — the topstrip's tallest line starts at that
   padding edge — so the chip sits there and the narrow block below hands
   the bottom corner back, where the cards go static anyway. */
.dro-alerts-arm {
    position: fixed;
    right: calc(var(--dro-scale) * 1.36);
    top: calc(var(--dro-scale) * 0.68);
    z-index: 2;
    padding: 0.5em 1em;
    border: none;
    border-radius: 999px;
    background: rgba(17, 24, 39, 0.75);
    color: var(--wa-color-neutral-0, #f9fafb);
font-size: max(0.9rem, 0.5em);
}

.dro-alerts-arm--armed {
    display: none;
}

/* The chip is rendered in dro/home.html's door_extras block, after </main> —
   a sibling of the panel, so the photo-frame rules above (which only reach
   [data-dro-current-view] descendants) never touch it. Without this, 't' for
   a bare photo frame left "Enable alerts" floating over the photo for the
   whole of any visit where the chip has nothing left to arm and so does not
   hide itself. Sibling combinator because .dro-text-off lands on the panel. */
.dro-text-off~.dro-alerts-arm {
    display: none;
}

/* The per-screen settings dialog ('o', static/js/oak-ed.js). A UA <dialog>
   is white-on-black-text and sized in px; the wall is dark and sized from
   --dro-scale, so both are restated. Floor of 1rem: someone is standing at
   the screen to use this, unlike the rest of the wall, so it must stay
   legible at a zoom chosen for reading from across the room. */
.dro-settings {
    /* Stated, not inherited from the kit: under content-box the side
       padding lands on top of the 92vw arm and a phone overflows. */
    box-sizing: border-box;
    width: min(34em, 92vw);
    max-height: 88vh;
    padding: 1.4em 1.6em;
    border: none;
    border-radius: 0.6em;
    background: rgba(17, 24, 39, 0.96);
    color: var(--wa-color-neutral-0, #f9fafb);
    font-size: max(1rem, calc(var(--dro-scale) * 0.64));
    overflow-y: auto;
}

.dro-settings::backdrop {
    background: rgba(0, 0, 0, 0.5);
}

.dro-settings__header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1em;
}

.dro-settings__title {
    margin: 0;
    font-size: 1.4em;
}

.dro-settings__note,
.dro-settings__reason {
    margin: 0.3em 0 0;
    color: var(--wa-color-neutral-300, #d1d5db);
    font-size: 0.85em;
}

.dro-settings__section {
    margin-top: 1.2em;
    padding-top: 0.9em;
    border-top: 1px solid rgba(249, 250, 251, 0.2);
}

.dro-settings__heading {
    margin: 0 0 0.5em;
    font-size: 1.1em;
}

.dro-settings__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1em;
    margin: 0.45em 0;
}

/* display: flex above beats the UA's [hidden] rule, as the dial's own
   display: grid does — and the notification "Allow" row is shown and
   hidden by that attribute, so say it again here too. */
.dro-settings__row[hidden] {
    display: none;
}

/* Most rows are <label>s, and the WebAwesome kit's native styles reach
   them: every label gets the kit's dark form-label colour (invisible on
   this panel) and a checkbox's label is shrunk to its text (so
   space-between had no space, and each box sat wherever its sentence
   ended). Two classes, to outrank the kit's element-and-attribute
   selectors rather than depend on how it happens to layer them. The range
   loses the top margin the kit adds for a label stacked above it; here the
   label sits beside it. */
.dro-settings label.dro-settings__row {
    width: 100%;
    color: inherit;
}

/* ...and the fixed width the kit gives it, which squeezed "Text size" onto
   two lines: the slider takes what the label's text leaves. */
.dro-settings .dro-settings__row input[type="range"] {
    flex: 1 1 8em;
    width: auto;
    min-width: 0;
    margin: 0;
}

.dro-settings__row input[type="number"] {
    width: 5em;
}

/* The kit stretches a <select> to its container; four short options do not
   need the whole row. */
.dro-settings .dro-settings__row select {
    width: auto;
}

.dro-settings__close,
.dro-settings__button {
    padding: 0.35em 0.9em;
    border: 1px solid rgba(249, 250, 251, 0.4);
    border-radius: 999px;
    background: transparent;
    color: inherit;
    font: inherit;
}

.dro-settings__keys {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.35em 1em;
    margin: 0;
}

.dro-settings__keys dd {
    margin: 0;
}

/* The settings button: nothing at all unless the primary input is a finger.
   Bottom centre because every corner is spoken for (clock, alerts chip or
   dial, and the two cards). 2.75rem is the 44px minimum tap target. */
.dro-settings-open {
    display: none;
}

@media (hover: none) and (pointer: coarse) {
    .dro-settings-open {
        display: block;
        position: fixed;
        left: 50%;
        bottom: calc(var(--dro-scale) * 0.9);
        transform: translateX(-50%);
        z-index: 2;
        min-width: 2.75rem;
        min-height: 2.75rem;
        padding: 0;
        border: none;
        border-radius: 50%;
        background: rgba(17, 24, 39, 0.55);
        color: var(--wa-color-neutral-0, #f9fafb);
        font-size: max(1.25rem, calc(var(--dro-scale) * 1));
        line-height: 1;
        opacity: 0.55;
    }

    /* A cheat-sheet for keys this screen does not have. */
    .dro-settings__section--keys {
        display: none;
    }

    /* Inside the dialog a finger has to hit a native checkbox or select, on
       a row otherwise as tall as its own text. The same 44px minimum the
       button above uses, and the dialog scrolls (max-height: 88vh), so the
       rows are free to be taller than a phone's screen.
       NOTE: 88vh rather than the spec's 100dvh on purpose. vh is the LARGE
       viewport, so 88vh already leaves room for a toolbar that dvh would let
       the dialog slide under, and a dialog flush to both edges of a phone
       reads as a page rather than something dismissible. */
    .dro-settings__row {
        min-height: 2.75rem;
    }
}

.dro-settings kbd {
    padding: 0.05em 0.4em;
    border: 1px solid rgba(249, 250, 251, 0.4);
    border-radius: 0.25em;
    font-family: inherit;
}

/* The pomodoro dial (templates/dro/base.html, static/js/oak-ed.js). Fixed
   and above the corner cards, like the alerts chip; sized from --dro-scale
   so the zoom keys carry it (6.8 × the knob is about 18% of a 16:9 wall's
   short side at zoom 1). One custom property carries the phase colour to
   the ring and the label. Literal colours, not --wa-color-* tokens: the
   kit's red/green scales are not set on these chrome-free doors. */
.dro-pomodoro {
    --dro-pomodoro-color: #9ca3af;
    position: fixed;
    z-index: 2;
    display: grid;
    place-items: center;
    width: calc(var(--dro-scale) * 6.8);
    height: calc(var(--dro-scale) * 6.8);
    border-radius: 50%;
    background: rgba(17, 24, 39, 0.8);
    color: var(--wa-color-neutral-0, #f9fafb);
    pointer-events: none;
}

/* display: grid above beats the UA's [hidden] rule, so say it again. */
.dro-pomodoro[hidden] {
    display: none;
}

.dro-pomodoro[data-phase="stopped"] {
    --dro-pomodoro-color: #9ca3af;
}

.dro-pomodoro[data-phase="work"] {
    --dro-pomodoro-color: #ef4444;
}

.dro-pomodoro[data-phase="break"] {
    --dro-pomodoro-color: #22c55e;
}

/* NOTE: the top pair's offset is a tuned clearance, not a computed one: it
   clears the topstrip (and, on the session door, the alerts chip in the
   band above it). top-left still lands on the daily agenda, which is
   left-aligned — a preset for a wall that is mostly weekly, or a photo
   frame. */
.dro-pomodoro[data-corner="top-right"] {
    top: calc(var(--dro-scale) * 7.3);
    right: calc(var(--dro-scale) * 1.36);
}

.dro-pomodoro[data-corner="top-left"] {
    top: calc(var(--dro-scale) * 7.3);
    left: calc(var(--dro-scale) * 1.36);
}

/* The bottom pair stands BESIDE its corner card, on the same bottom edge,
   not above it. A card's height follows its content (a library notice with
   a cover measured 11.4 scale units on a live wall, an art card more), so
   no fixed height clears it; its width is bounded — max-width 19em at a
   0.73 font is 13.87 units, plus the 1.36 edge — so 16.2 always does. */
.dro-pomodoro[data-corner="bottom-right"] {
    bottom: calc(var(--dro-scale) * 1.36);
    right: calc(var(--dro-scale) * 16.2);
}

.dro-pomodoro[data-corner="bottom-left"] {
    bottom: calc(var(--dro-scale) * 1.36);
    left: calc(var(--dro-scale) * 16.2);
}

/* ...and takes the corner itself when there is no card to stand beside:
   none rendered (nothing reviewed yet, no of-the-day item), hidden by this
   screen's settings, or stripped by 't'. Sibling combinators because the
   dial follows the panel these classes and cards live on. */
.dro-display:not(:has(.dro-of-the-day))~.dro-pomodoro[data-corner="bottom-right"],
.dro-hide-of-the-day~.dro-pomodoro[data-corner="bottom-right"],
.dro-text-off~.dro-pomodoro[data-corner="bottom-right"] {
    right: calc(var(--dro-scale) * 1.36);
}

.dro-display:not(:has(.dro-library))~.dro-pomodoro[data-corner="bottom-left"],
.dro-hide-library~.dro-pomodoro[data-corner="bottom-left"],
.dro-text-off~.dro-pomodoro[data-corner="bottom-left"] {
    left: calc(var(--dro-scale) * 1.36);
}

.dro-pomodoro__ring {
    grid-area: 1 / 1;
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
}

.dro-pomodoro__track,
.dro-pomodoro__progress {
    fill: none;
    stroke-width: 7;
}

.dro-pomodoro__track {
    stroke: rgba(249, 250, 251, 0.15);
}

.dro-pomodoro__progress {
    stroke: var(--dro-pomodoro-color);
    transition: stroke 0.4s;
}

.dro-pomodoro__face {
    grid-area: 1 / 1;
    display: grid;
    justify-items: center;
    line-height: 1.1;
}

.dro-pomodoro__time {
    font-size: calc(var(--dro-scale) * 1.7);
    font-variant-numeric: tabular-nums;
}

/* A work phase may be set to 120 minutes, and "120:00" is six tabular
   glyphs where the ring leaves room for five. static/js/oak-ed.js sets
   data-wide whenever the readout grows that sixth glyph; 5/6 of the usual
   size puts it back inside the stroke. */
.dro-pomodoro[data-wide] .dro-pomodoro__time {
    font-size: calc(var(--dro-scale) * 1.4);
}

.dro-pomodoro__label {
    color: var(--dro-pomodoro-color);
    font-size: calc(var(--dro-scale) * 0.6);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.dro-empty {
    color: var(--wa-color-neutral-300, #d1d5db);
    font-style: italic;
}

/* Dead display link: friendly, centered, nothing alarming. */
.dro-inactive {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 2rem;
    gap: 0.5rem;
}

/* Narrow viewports (the session door on a phone). The wall layout — seven
   equal columns under two position: fixed corner cards — is unusable below
   roughly a tablet, so only the geometry changes here; the TV keeps the rules
   above untouched. */
@media (max-width: 48rem) {
    /* Under the agenda, not beside it; and twelve columns of numbers have to
       be reachable on a phone rather than crushed. */
    .dro-view--charted {
        grid-template-columns: minmax(0, 1fr);
    }

    /* The floor is what makes the scroll real: the wall's hour tracks are
       minmax(0, 1fr) and shrink to nothing, so the grid fits any container
       and overflow-x had nothing to scroll — 360px divided twelve ways, with
       "100%" and each wind value spilling out of its own cell. */
    .dro-chart {
        grid-template-columns: max-content repeat(var(--dro-chart-columns, 12), minmax(2.5em, 1fr));
        overflow-x: auto;
    }

    .dro-week {
        grid-template-columns: 1fr;
    }

    .dro-week__day {
        min-height: 0;
    }

    /* Static, not fixed: pinned to the corners they covered the agenda, and
       on a scrolling page there is nothing to scroll them out of the way. */
    .dro-of-the-day,
    .dro-library {
        position: static;
        max-width: none;
        margin-top: calc(var(--dro-scale) * 1.36);
    }

    /* The cards above are no longer in the corners, so neither bottom preset
       has anything to stand beside — and 16.2 units is most of a phone. */
    .dro-pomodoro[data-corner="bottom-right"] {
        right: calc(var(--dro-scale) * 1.36);
    }

    .dro-pomodoro[data-corner="bottom-left"] {
        left: calc(var(--dro-scale) * 1.36);
    }

    /* The hourly strip is capped server-side for the TV, where clipping is
       right; on a phone the remaining hours have to be reachable. */
    .dro-forecast__hourly {
        overflow-x: auto;
    }

    /* Back to the bottom corner: the cards above have gone static and scroll
       away with the page, while the topstrip's clock and date now run the
       full width of the screen — straight through where the chip sits on the
       wall. Floating over scrolling content is what a phone expects here. */
    .dro-alerts-arm {
        top: auto;
        right: 1rem;
        bottom: 1rem;
    }
}

/* Teacher-surface DRO Setup page bits. */
.dro-color-swatch {
    display: inline-block;
    width: 1.2em;
    height: 1.2em;
    border-radius: 0.3em;
    border: 1px solid var(--wa-color-neutral-300, #d1d5db);
    vertical-align: middle;
}

.dro-token-list {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
}

.dro-photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.dro-photo-tile {
    margin: 0;
    padding: 0.75rem;
    border: 1px solid var(--wa-color-neutral-200, #e5e7eb);
    border-radius: 0.4em;
}

.dro-photo-tile figcaption { margin-bottom: 0.5rem; }

/* Remove is irreversible and the caption is optional, so the picture is what
   identifies the tile. Fixed height keeps the grid even across portrait and
   landscape uploads. */
.dro-photo-tile__image {
    display: block;
    width: 100%;
    height: 7rem;
    object-fit: cover;
    border-radius: 0.3em;
    margin-bottom: 0.5rem;
}

/* --- DRO print ---
   templates/dro/print.html: today's agenda and the rolling week as two
   named page boxes, so a Chromium print job gives the daily page portrait
   and the weekly page landscape without the teacher touching the printer
   dialog (Firefox's per-named-page @page support is patchy — Chromium is
   the target). No .dro-background photo layer here at all — a printed page
   has no rotation to sit behind, so there is no page background to worry
   about, only the two sections' own light ground. */
@page daily {
    size: A4 portrait;
    margin: 12mm;
}

@page weekly {
    size: A4 landscape;
    margin: 12mm;
}

.dro-print__page--daily {
    page: daily;
}

.dro-print__page--weekly {
    page: weekly;
    break-before: page;
}

.dro-print-body {
    margin: 0;
    background: var(--wa-color-neutral-0, #ffffff);
    color: var(--wa-color-neutral-950, #111827);
    font-family: 'EB Garamond', Georgia, serif;
    font-size: 11pt;
}

.dro-print__toolbar {
    display: flex;
    align-items: baseline;
    gap: 1rem;
    padding: 1rem;
    border-bottom: 1px solid var(--wa-color-neutral-200, #e5e7eb);
}

/* On screen the two pages just stack, with a border standing in for the
   sheet edge a printer would otherwise cut at, and a padding standing in
   for the margin only a real @page has. Both are dropped under @media
   print below, where the page's own @page margin already does the job —
   a printed border would waste ink on every sheet, and the padding would
   stack on that margin rather than replace it. */
.dro-print__page {
    box-sizing: border-box;
    max-width: 210mm;
    margin: 1rem auto;
    padding: 12mm;
    border: 1px solid var(--wa-color-neutral-200, #e5e7eb);
}

.dro-print__page--weekly {
    max-width: 297mm;
}

.dro-print__location {
    margin: 0 0 4mm;
    color: var(--wa-color-neutral-600, #4b5563);
}

/* The wall panel's own .dro-agenda/.dro-week/.dro-forecast rules are tuned
   for a dark TV at a distance — clamp()-scaled type tracking the viewport,
   and colours meant to sit on rgba(17,24,39,...) glass. An A4 sheet is a
   fixed physical size, so the print body overrides every clamp() with a
   plain pt/mm size, and print-color-adjust keeps the feed-colour stripes
   and WHO-band UV chips in colour instead of the greyscale a browser
   defaults printing to. */
.dro-print-body .dro-agenda,
.dro-print-body .dro-week,
.dro-print-body .dro-forecast,
.dro-print-body .dro-uv {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
}

.dro-print-body .dro-agenda {
    font-size: 11pt;
    max-width: none;
    width: auto;
}

/* The wall's translucent dark panel is for reading over a photo; the sheet
   has none. Not re-tinted with a light token either: this kit's neutral-50
   is a mid grey (WA's 50 is mid-lightness), so every row printed as a grey
   bar. Paper is the background; the row keeps its brand left rule. */
.dro-print-body .dro-agenda__item {
    background: none;
    color: inherit;
}

/* The running-event tint (.dro-agenda__item--running / .dro-week__event--running
   above) is the same translucent grey. The agenda row is already out-specified
   by the rule above; the week event has no row rule, so a schedule printed
   mid-event kept one grey bar. Bold alone marks it on paper. */
.dro-print-body .dro-week__event--running {
    background: none;
}

.dro-print-body .dro-agenda__time,
.dro-print-body .dro-agenda__where {
    color: var(--wa-color-neutral-600, #4b5563);
}

/* A finished row fades to 45% on the wall, where the reader is across the
   room and what is next matters more than what is done. A sheet is read up
   close and cannot be re-rendered at another time: an evening printout of
   today is an agenda that has entirely ended, and #111827 at 45% composites
   to about 2.9:1 on the paper — a page that looks like the toner ran out.
   "Done" still says so here, in the same grey the time and place already
   print in, against the near-black of everything still to come. */
.dro-print-body .dro-agenda__item--ended,
.dro-print-body .dro-week__event--ended {
    opacity: 1;
    color: var(--wa-color-neutral-600, #4b5563);
}

/* "Nothing on today." is the whole of a clear day's page (view_daily.html's
   else-branch, and panel.html's own no-household line). The wall's grey is
   ~1.5:1 on this white ground, which prints as a blank sheet — nothing to
   tell the teacher the agenda rendered at all rather than the job failing. */
.dro-print-body .dro-empty {
    color: var(--wa-color-neutral-600, #4b5563);
}

.dro-print-body .dro-forecast {
    font-size: 9pt;
    color: inherit;
}

.dro-print-body .dro-forecast__label {
    color: var(--wa-color-neutral-600, #4b5563);
}

/* .dro-forecast__hourly's overflow: hidden; white-space: nowrap is the wall's
   deliberate clipping (a TV must never scroll); on a fixed A4 sheet that
   clips a full hourly strip off the paper with no scrollbar and no
   indication, so print wraps instead. The UV chips' own colours
   (.dro-forecast__uv, .dro-uv--*) were chosen to read on the wall's dark
   ground and stay a direct declaration under print-color-adjust: exact,
   so they need a paper-legible override of their own rather than relying
   on .dro-forecast's inherited colour. Two tiers of it: a dark neutral for
   a chip whose band is unknown, then the bands themselves below. */
.dro-print-body .dro-forecast__hourly {
    white-space: normal;
    overflow: visible;
}

.dro-print-body .dro-forecast__uv {
    color: var(--wa-color-neutral-800, #1f2937);
}

/* The topstrip's spelling of the same chip (panel.html renders it as
   `dro-uv dro-uv--<band>`, the forecast chips as `dro-forecast__uv
   dro-uv--<band>`). The print page carries no topstrip, so on paper this
   one is only ever reached if that markup moves; the fallback the printed
   chips actually take is .dro-forecast__uv above. */
.dro-print-body .dro-uv {
    color: var(--wa-color-neutral-800, #1f2937);
}

/* The WHO scale is carried by colour — the label rides only the current
   reading, as the wall's own comment above .dro-uv--low says — so a single
   dark neutral for every chip would print all seven weekly columns
   identical, which is exactly what print-color-adjust: exact was set to
   avoid. Same hue order as the wall, re-mixed for white paper: these are
   ink values, not the wall's dark-ground tints (all of which land under
   2:1 here) and not WA tokens, whose kit-supplied scale is tuned mid-
   lightness. Each reads between 4.7:1 and 6.7:1 on the sheet.

   Specificity ties both fallbacks above at (0,2,0), so their position after
   them in the file is the whole of why they win — keep them here. The one
   that decides the sheet is .dro-print-body .dro-forecast__uv: that is the
   class the printed chips carry, the bare .dro-uv being the topstrip's. */
.dro-print-body .dro-uv--low { color: #15803d; }
.dro-print-body .dro-uv--moderate { color: #a16207; }
.dro-print-body .dro-uv--high { color: #c2410c; }
.dro-print-body .dro-uv--very-high { color: #b91c1c; }
.dro-print-body .dro-uv--extreme { color: #7e22ce; }

.dro-print-body .dro-forecast__rain { color: #1d4ed8; }
.dro-print-body .dro-week__pollen { color: #c2410c; }

/* The weekly grid spans the full landscape page width rather than the
   wall's viewport-relative gap. grid-template-columns is re-declared, not
   just width/gap: in paged media Chromium measures the wall's narrow
   (max-width: 48rem) breakpoint against the @page daily portrait page area
   (~703px, under the threshold), which would otherwise collapse this grid
   to one column for the whole print run — see .dro-week's own comment
   above the breakpoint. (0,2,0) beats that (0,1,0) rule regardless of how
   the query evaluates. */
.dro-print-body .dro-week {
    grid-template-columns: repeat(7, 1fr);
    width: 100%;
    gap: 3mm;
}

/* background: none for the same reason as .dro-agenda__item above; today's
   column keeps its outline, the rest are set apart by the gap alone. */
.dro-print-body .dro-week__day {
    background: none;
    min-height: 0;
}

.dro-print-body .dro-week__day--today {
    outline-color: var(--wa-color-brand-600, #2563eb);
}

.dro-print-body .dro-week__day h3,
.dro-print-body .dro-week__weather {
    color: var(--wa-color-neutral-600, #4b5563);
}

@media print {
    .dro-print__toolbar {
        display: none;
    }

    /* padding with the rest: stacked on @page's own 12mm it inset every
       sheet by 24mm a side, taking 24mm off the width the landscape weekly
       grid sizes its seven columns against. */
    .dro-print__page {
        margin: 0;
        max-width: none;
        padding: 0;
        border: none;
    }

    /* Screen furniture, never part of a page. */
    .dro-settings,
    .dro-settings-open,
    .dro-pomodoro {
        display: none;
    }
}

.report-status__notice {
    font-size: 0.875rem;
    color: var(--wa-color-neutral-600, #4b5563);
    margin-top: 0.25rem;
}
