/**
 * modals.css — appearance of the shared dialogs and toasts.
 * Version 2.0.0. Partner to _core/js/unifiedModals.js.
 *
 * ═══════════════════════════════════════════════════════════════════════════
 *  WHAT THIS FILE IS, AND WHAT IT IS NOT
 * ═══════════════════════════════════════════════════════════════════════════
 * This is the STRUCTURE of a dialog — its shape, spacing, layering and
 * behaviour. It is byte-identical in all six School apps, synced from
 * `_core/css/` by `_core/sync.mjs`. An edit made inside an app is overwritten
 * on the next sync; edit the copy in `_core/css/`.
 *
 * It is NOT the palette. Every colour here is read from one of the app's own
 * design tokens, so six apps share one dialog while each keeps its identity.
 *
 * ── THE RULE THAT MATTERS, AND THE BUG THAT PRODUCED IT ────────────────────
 * This file may only use tokens that EVERY app defines for EVERY theme, and
 * it may never pair two tokens unless both come from that set.
 *
 * The first version of this file broke that rule. It wrote
 *
 *     color: var(--danger, #b3261e);
 *     background: var(--danger-soft, #fdecea);
 *
 * which looks safe and is not. Most of these apps define `--danger` in both
 * light and dark, but none of them defines `--danger-soft` at all. So in dark
 * mode the text took the app's light-on-dark red and the background took a
 * hard-coded near-white — 1.66:1, against a AAA target of 7:1. The same
 * mismatch made the close button 1.78:1.
 *
 * An inline fallback CANNOT fix this, because a literal colour cannot know
 * which theme is showing. The answer is not better fallbacks; it is to stop
 * needing them:
 *
 *   - Surfaces and text come from `--panel`, `--ink`, `--muted`, `--line`,
 *     `--line-strong` — the tokens every app defines per theme.
 *   - Status colours come from `--danger`, `--warning`, `--success`,
 *     `--accent`, shown ON `--panel`. Both halves are the app's, so both
 *     change together.
 *   - Tints and hovers are derived from those with `color-mix()`, inside an
 *     `@supports` block, so they can only ever be a shade of a colour that is
 *     already correct for the theme.
 *
 * Literal values remain only where they cannot affect contrast: shadows, radii
 * and the backdrop.
 *
 * ── LOAD ORDER ─────────────────────────────────────────────────────────────
 * Load this BEFORE the app's own stylesheets. Two reasons:
 *   1. The app's `:root` tokens then override the fallbacks here.
 *   2. Section 6's dialog buttons are deliberately more specific than a bare
 *      `.btn`, so an app's own button rule cannot half-override them. See the
 *      note there.
 *
 * ── THE TOKENS IT READS ────────────────────────────────────────────────────
 * Surfaces   --panel --line --line-strong
 * Text       --ink --muted
 * Status     --accent --danger --warning --success
 * Shape      --radius --radius-sm --shadow-2 --shadow-3
 * Sizing     --min-touch-target --font-size-sm --bottombar-h
 * Layering   --z-toast
 *
 * Eleven tokens fewer than the first version, and every one that remains is
 * defined by all six apps in both themes. The `-soft` variants, `--panel-2`
 * and `--ink-soft` are gone: those were the ones the apps did not define, and
 * they were the whole of the bug described above.
 *
 * `--z-modal` is deliberately absent. A dialog opened with `showModal()` is
 * promoted to the browser's top layer, which sits above every element on the
 * page whatever its `z-index`. There is no stacking order left to manage.
 */

/* ══════════════════════════════════════════════════════════════════════════
   SECTION 1 — THE DIALOG
   ══════════════════════════════════════════════════════════════════════════ */

/**
 * The panel itself.
 *
 * A `<dialog>` arrives with opinionated user-agent styles — padding, a solid
 * border, `width: fit-content`, and `display: none` until it is opened. Most
 * of what follows is replacing those rather than adding to a blank element.
 *
 * `margin: auto` is what centres a modal dialog, both axes at once. It comes
 * from the UA stylesheet and is restated here so that an app-level
 * `* { margin: 0 }` reset cannot silently push every dialog into the corner.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   [hidden] MUST WIN
   ═══════════════════════════════════════════════════════════════════════════
   `hidden` is how every one of these apps hides a control it has decided the
   child should not see: `card.hidden = true`. It works because the USER-AGENT
   stylesheet says `[hidden] { display: none }` — and any author `display`
   rule, at any specificity, beats the user agent. So one line like

       .unified-option { display: flex; }

   silently turns `hidden` into a no-op for every element carrying that class,
   and the code doing the hiding goes on looking correct.

   ── WHAT THIS COST ────────────────────────────────────────────────────────
   School Robots offers "Quick Save" in its save dialog — update the cloud
   file you already have open. `localFileTools.js` builds it hidden, and
   `updateSaveCloudCard()` re-checks on every open whether there is a cloud
   file to update. All of that was right. `.unified-option { display: flex }`
   made it visible anyway, from the first save of a brand-new program, before
   there was anything to quick-save. Measured in Chromium against the app's
   own stylesheets: the card computed to `display: flex`, 1280 x 84, on
   screen. Reported by a teacher in September 2026.

   It was reachable from four of the six apps. Only School Paint and School
   Words carried a rule like this one; the four that did not — Data, Draw,
   Robots and Sounds — each had at least one class with a `display` that
   would defeat `hidden` on any element built by script.

   ── WHY HERE ──────────────────────────────────────────────────────────────
   `_core/css/modals.css` is the one stylesheet all six application pages
   load, and the application page is where every script-built control in
   these apps lives. Putting it here means no app can be added without it.

   `!important` is deliberate and is the point: it must beat a `display` an
   app sets on the same element, which is the whole failure mode. Nothing
   should ever want an element that is `hidden` to be laid out. To reveal
   one, remove the attribute — that is what it is for.

   `_core/test/hiddenIsHidden.test.mjs` fails any app whose application page
   does not reach a rule like this. */
[hidden] {
  display: none !important;
}

dialog.modal-panel {
  margin: auto;
  padding: 0;
  width: min(38rem, 92vw);
  max-width: 92vw;
  max-height: 88vh;
  overflow: hidden;

  /* `color` here is what makes `color: inherit` safe further down: everything
     inside the dialog descends from the app's own ink colour. */
  color: var(--ink, #16202b);
  background: var(--panel, #ffffff);
  border: 1px solid var(--line-strong, #93a3b5);
  border-radius: var(--radius, 12px);
  box-shadow: var(--shadow-3, 0 12px 32px rgba(16, 32, 43, 0.22));
}

/**
 * THE IMPORTANT ONE. The layout must be applied only while the dialog is open.
 *
 * A closed `<dialog>` is hidden by the UA rule `dialog:not([open]) {
 * display: none }`. Putting `display: flex` in the rule above would override
 * that and leave every dialog in the app permanently visible, stacked down the
 * page. The `[open]` attribute selector is what keeps the two states apart.
 */
dialog.modal-panel[open] {
  display: flex;
  flex-direction: column;
}

/** The wider variant, for file lists and side-by-side content. */
dialog.modal-panel.wide {
  width: min(56rem, 94vw);
}

/**
 * The dimmed page behind.
 *
 * `::backdrop` is drawn by the browser for a dialog in the top layer, and
 * replaces the overlay element a hand-rolled dialog needs. It cannot be
 * clicked as a target in its own right — which is why unifiedModals.js works
 * out backdrop clicks from geometry rather than from `event.target`.
 */
dialog.modal-panel::backdrop {
  background: rgba(12, 20, 28, 0.55);
  /* A slight blur separates the layers. Dropped under the reduced-transparency
     and reduced-motion preferences in Section 5. */
  backdrop-filter: blur(2px);
}

/* ── Header ─────────────────────────────────────────────────────────────── */

.modal-header {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  background: var(--panel, #ffffff);
  border-bottom: 1px solid var(--line, #ccd6e0);
  position: sticky;
  top: 0;
}

/**
 * Wraps the title and subtitle so the close button can sit beside them.
 * `min-width: 0` lets a long title wrap instead of pushing the close button
 * off the edge — the default `min-width: auto` on a flex child refuses to
 * shrink below its content, which is rarely what anyone wants.
 */
.modal-heading-group {
  flex: 1;
  min-width: 0;
}

.modal-title {
  margin: 0;
  font-size: 1.25rem;
  overflow-wrap: anywhere;
}

.modal-subtitle {
  margin: 0.15rem 0 0;
  color: var(--muted, #55647a);
  font-size: var(--font-size-sm, 0.875rem);
}

/**
 * `--min-touch-target` defaults to 44px: the smallest target WCAG 2.5.8 will
 * accept, and about the width of an eight-year-old's fingertip.
 */
.modal-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--min-touch-target, 44px);
  min-height: var(--min-touch-target, 44px);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm, 8px);
  /* Inherits the dialog's `--ink`, so it is correct in every theme by
     construction. This was `var(--ink-soft, #3d4a59)`, and in dark mode the
     fallback put a light-theme grey on a dark panel: 1.78:1. */
  color: inherit;
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
}

.modal-close:hover {
  border-color: var(--line, #ccd6e0);
}

/* ── Body ───────────────────────────────────────────────────────────────── */

/**
 * The only scrolling region. `min-height: 0` is the companion to `flex: 1 1
 * auto` — without it a flex child will not shrink below its content, the body
 * never becomes scrollable, and a long dialog grows past the bottom of the
 * screen with its footer out of reach.
 */
.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
  scrollbar-width: thin;
  overscroll-behavior: contain;
}

/* ── Footer ─────────────────────────────────────────────────────────────── */

.modal-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: flex-end;
  padding: 0.75rem 1rem;
  background: var(--panel, #ffffff);
  border-top: 1px solid var(--line, #ccd6e0);
  position: sticky;
  bottom: 0;
}

/** `{ spacer: true }` in a button list: pushes what follows to the right. */
.modal-footer .spacer {
  flex: 1;
}

/* ══════════════════════════════════════════════════════════════════════════
   SECTION 2 — ALERT BANNERS
   Used by `UnifiedModals.alert()`. Each states its kind in words as well as
   colour, so the meaning does not depend on being able to tell red from amber
   (WCAG 1.4.1 Use of Colour). The border carries the same information again,
   for anyone who sees no colour at all.
   ══════════════════════════════════════════════════════════════════════════ */

.alert {
  display: flex;
  gap: 0.6rem;
  padding: 0.7rem 0.85rem;
  margin-bottom: 1rem;
  border: 2px solid var(--line-strong, #93a3b5);
  border-radius: var(--radius-sm, 8px);
  font-size: var(--font-size-sm, 0.875rem);
  /* No background. The text colour below sits on the dialog's own `--panel`,
     so the contrast pair is two tokens the app controls together. A tint is
     added back in the @supports block underneath, derived from those same two
     colours so it cannot drift from the theme. */
  background: transparent;
}

/**
 * The status colour lives in the BORDER and the tint, never in the text.
 *
 * Colouring the text with `--danger` or `--accent` looks right and fails in
 * the dark. School Draw and School Paint set `--accent: #6366f1` — an indigo
 * chosen for filled buttons — and on their dark `--panel: #2d2d2d` that is
 * 3.08:1, under the 4.5:1 those apps target. The same trap as the buttons
 * below: a token picked for one job used for another.
 *
 * `--ink` on `--panel` is the pairing every app has already had to get right
 * for its body text, so the words are always readable. The kind of message is
 * still carried twice over — by the border colour, and by the word the module
 * puts in front of it ("Problem:", "Careful:", "Done:", "Note:") — so nothing
 * depends on telling red from amber (WCAG 1.4.1).
 */
.alert-error,
.alert-warning,
.alert-success,
.alert-info {
  color: var(--ink, #16202b);
}

.alert-error   { border-color: var(--danger, #b3261e); }
.alert-warning { border-color: var(--warning, #8a4b00); }
.alert-success { border-color: var(--success, #0b6e4f); }
.alert-info    { border-color: var(--accent, #0f766e); }

/**
 * The tint, for browsers that can mix colours.
 *
 * 12% of the status colour into the panel colour: enough to read as a banner,
 * light enough that it barely moves the contrast ratio it sits behind. Both
 * inputs are the app's own tokens, so in dark mode this is 12% of the app's
 * dark-mode red into the app's dark-mode panel — which is what the old
 * hard-coded `#fdecea` could never be.
 */
@supports (background: color-mix(in srgb, red 12%, white)) {
  .alert-error {
    background: color-mix(in srgb, var(--danger, #b3261e) 12%, var(--panel, #fff));
  }
  .alert-warning {
    background: color-mix(in srgb, var(--warning, #8a4b00) 12%, var(--panel, #fff));
  }
  .alert-success {
    background: color-mix(in srgb, var(--success, #0b6e4f) 12%, var(--panel, #fff));
  }
  .alert-info {
    background: color-mix(in srgb, var(--accent, #0f766e) 12%, var(--panel, #fff));
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   SECTION 3 — CHOICE LISTS
   Used by `UnifiedModals.choose()`. Large targets, because the people using
   these apps are learning to aim.
   ══════════════════════════════════════════════════════════════════════════ */

.choice-list {
  display: grid;
  gap: 0.6rem;
}

.choice {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  width: 100%;
  min-height: var(--min-touch-target, 44px);
  padding: 0.8rem 0.9rem;
  background: var(--panel, #ffffff);
  border: 2px solid var(--line-strong, #93a3b5);
  border-radius: var(--radius-sm, 8px);
  text-align: left;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

.choice:hover:not(:disabled) {
  border-color: var(--accent, #0f766e);
}

@supports (background: color-mix(in srgb, red 6%, white)) {
  .choice:hover:not(:disabled),
  .modal-close:hover {
    /* A 6% wash of the app's ink over the app's panel: a hover state that is
       darker in a light theme and lighter in a dark one, without either being
       written down. */
    background: color-mix(in srgb, var(--ink, #16202b) 6%, var(--panel, #fff));
  }
}

.choice:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.choice-icon {
  flex: 0 0 32px;
  width: 32px;
  height: 32px;
}

.choice-text {
  flex: 1;
  min-width: 0;
}

.choice-name {
  display: block;
  font-weight: 700;
}

.choice-desc {
  display: block;
  color: var(--muted, #55647a);
  font-size: var(--font-size-sm, 0.875rem);
}

/**
 * A choice that needs signing in first says so, without the calling code
 * having to remember to append the sentence.
 */
.choice[data-requires-auth="true"] .choice-desc::after {
  content: " Sign in first to use this.";
  font-weight: 600;
  color: var(--warning, #8a4b00);
}

/** Shown by `choose()` when there is nothing in the list. */
.empty-note {
  padding: 1.5rem 1rem;
  text-align: center;
  color: var(--muted, #55647a);
}

/* ══════════════════════════════════════════════════════════════════════════
   SECTION 4 — TOASTS
   ══════════════════════════════════════════════════════════════════════════ */

/**
 * The stack sits above the app's bottom bar where there is one. Apps without a
 * bottom bar leave `--bottombar-h` undefined and the toasts sit 1rem from the
 * bottom of the window.
 *
 * `pointer-events: none` on the container, `auto` on each toast: the gaps
 * between toasts stay clickable, so a stack of messages never blocks the tool
 * underneath it.
 */
.toast-stack {
  position: fixed;
  left: 50%;
  bottom: calc(var(--bottombar-h, 0px) + 1rem);
  transform: translateX(-50%);
  z-index: var(--z-toast, 3000);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: min(30rem, 92vw);
  pointer-events: none;
}

.toast {
  color: var(--ink, #16202b);
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.7rem 0.9rem;
  background: var(--panel, #ffffff);
  border: 2px solid var(--line-strong, #93a3b5);
  border-left-width: 6px;
  border-radius: var(--radius-sm, 8px);
  box-shadow: var(--shadow-2, 0 4px 12px rgba(16, 32, 43, 0.12));
  pointer-events: auto;
}

.toast-success { border-left-color: var(--success, #0b6e4f); }
.toast-error   { border-left-color: var(--danger, #b3261e); }
.toast-warning { border-left-color: var(--warning, #8a4b00); }
.toast-info    { border-left-color: var(--accent, #0f766e); }

.toast-text {
  flex: 1;
  min-width: 0;
  /* A cloud filename can be one long unbroken string. Without this it pushes
     the dismiss button off the edge of the toast. */
  overflow-wrap: anywhere;
}

.toast-kind {
  font-weight: 800;
}

.toast-dismiss {
  min-width: var(--min-touch-target, 44px);
  min-height: var(--min-touch-target, 44px);
  background: transparent;
  border: 0;
  font-size: 1.1rem;
  cursor: pointer;
  color: inherit;
}

/* ══════════════════════════════════════════════════════════════════════════
   SECTION 5 — USER PREFERENCES
   Each of these responds to a setting the person has already chosen in their
   operating system. None of them needs a control in the app.
   ══════════════════════════════════════════════════════════════════════════ */

/**
 * Someone who has asked for less motion, or for less transparency, is telling
 * us the blur is unhelpful — it is a common trigger for visual discomfort and
 * it costs GPU time on the low-end hardware these apps often run on. The
 * dimming stays; only the blur goes.
 */
@media (prefers-reduced-motion: reduce), (prefers-reduced-transparency: reduce) {
  dialog.modal-panel::backdrop {
    backdrop-filter: none;
    background: rgba(12, 20, 28, 0.7);
  }
}

/**
 * Windows High Contrast Mode and its equivalents replace the palette wholesale
 * and discard background colours. Borders survive, so the parts of the design
 * that carry meaning are restated as borders here — otherwise the dialog loses
 * its edges and the alert banners become indistinguishable from body text.
 */
@media (forced-colors: active) {
  dialog.modal-panel {
    border: 2px solid CanvasText;
  }

  .modal-header,
  .modal-footer {
    border-color: CanvasText;
  }

  .alert,
  .choice,
  .toast {
    border-color: CanvasText;
  }

  .choice:hover:not(:disabled) {
    border-color: Highlight;
  }

  /* The toast's colour-coded left edge conveys nothing in forced colours, so
     the written prefix ("Problem:", "Careful:") does the work instead. */
  .toast {
    border-left-width: 2px;
  }
}

/**
 * A visible focus indicator on every interactive element in a dialog.
 * `:focus-visible` shows it for keyboard users and not for mouse users, which
 * is the behaviour that stops people removing focus rings altogether.
 */
.modal-close:focus-visible,
.modal-footer .btn:focus-visible,
.choice:focus-visible,
.toast-dismiss:focus-visible {
  outline: 3px solid var(--accent, #0f766e);
  outline-offset: 2px;
}

/* ══════════════════════════════════════════════════════════════════════════
   SECTION 6 — THE DIALOG'S BUTTONS
   ══════════════════════════════════════════════════════════════════════════
   unifiedModals.js gives its footer buttons the classes `btn`, `btn-primary`
   and `btn-danger`.

   ── WHY THESE SELECTORS ARE MORE SPECIFIC THAN THEY LOOK ───────────────────
   These rules used to be wrapped in `:where()`, contributing no specificity,
   so that an app's own `.btn` would win. That is a reasonable instinct and it
   produced a 1.06:1 button.

   School Draw's stylesheet has a `.btn` rule that sets `background: #f7f8fb`
   and no `color`, with no dark-mode variant. At equal specificity and loading
   later, it won on `background` and left this file's `color` in place. In dark
   mode that put white text on a near-white background. Neither stylesheet was
   wrong on its own; they were each right about half a button.

   A partial override is the hazard, and specificity is the only thing that
   prevents it. `dialog.modal-panel .modal-footer .btn` cannot be half-beaten
   by a bare `.btn`, so the two properties that have to agree always come from
   the same place.

   Every colour still comes from the app's own tokens, so this is shared
   structure with per-app colour rather than one look imposed on six apps. An
   app that wants something different overrides deliberately, with a selector
   at least as specific — and will then be setting both halves.
   ══════════════════════════════════════════════════════════════════════════ */

dialog.modal-panel .modal-footer .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--min-touch-target, 44px);
  padding: 0.5rem 1rem;
  font: inherit;
  font-weight: 600;
  color: var(--ink, #16202b);
  background: var(--panel, #ffffff);
  border: 2px solid var(--line-strong, #93a3b5);
  border-radius: var(--radius-sm, 8px);
  cursor: pointer;
}

dialog.modal-panel .modal-footer .btn:hover:not(:disabled) {
  border-color: var(--accent, #0f766e);
}

dialog.modal-panel .modal-footer .btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/**
 * The emphasised buttons.
 *
 * ── WHY THESE ARE NOT FILLED WITH THE ACCENT COLOUR ────────────────────────
 * The obvious design is white text on a solid `--accent` or `--danger` fill.
 * It was the first thing here, and it produced a 3.76:1 button in School
 * Sounds — below even the AA threshold of 4.5:1.
 *
 * These tokens are chosen by each app to be READ AS TEXT on a pale background.
 * School Sounds' `--danger` is `#ef4444`, a red that works as a warning label
 * and sits at 3.76:1 against white — the same 3.76:1 whether white is behind
 * it or on top of it. Using a foreground colour as a background does not
 * improve its contrast; it just moves it.
 *
 * So emphasis is carried by a tint, a heavier border and more weight, and the
 * TEXT stays `--ink` on `--panel` — the pairing every app has already had to
 * get right for its body copy.
 */
dialog.modal-panel .modal-footer .btn-primary,
dialog.modal-panel .modal-footer .btn-danger {
  font-weight: 700;
  border-width: 2px;
}

dialog.modal-panel .modal-footer .btn-primary {
  border-color: var(--accent, #0f766e);
}

dialog.modal-panel .modal-footer .btn-danger {
  border-color: var(--danger, #b3261e);
}

@supports (background: color-mix(in srgb, red 12%, white)) {
  dialog.modal-panel .modal-footer .btn:hover:not(:disabled) {
    background: color-mix(in srgb, var(--ink, #16202b) 6%, var(--panel, #fff));
  }
  dialog.modal-panel .modal-footer .btn-primary {
    background: color-mix(in srgb, var(--accent, #0f766e) 12%, var(--panel, #fff));
  }
  dialog.modal-panel .modal-footer .btn-danger {
    background: color-mix(in srgb, var(--danger, #b3261e) 12%, var(--panel, #fff));
  }
}

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