/* App typefaces. Declared here AND in css/app-settings.css AND in
   student/css/styles.css. No single stylesheet reaches every page: brand.css
   misses 404 / join / teacher-onboarding / fsmvu-loaders / lingualab-write,
   and app-settings.css misses index (login) and writing-exam. Together they
   cover all of them. Keep the three copies identical. */
:root {
  --ll-font: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --ll-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
}

/* Form controls do not inherit font-family - browsers give them their own
   default (Arial on Windows Chrome), which is what actually put a second
   typeface on screen next to the Segoe body text. font-family ONLY: `font:
   inherit` would also take size, and the control default is 13.33px, so that
   version moves layout. Specificity 0,0,1, so any class rule still wins.
   Mirrored in css/app-settings.css for the pages this file misses. */
button, input, select, textarea, optgroup {
  font-family: inherit;
}

/* ════════════════════════════════════════════════════════════════
   BRAND RESET - loaded on every page of the app.
   ----------------------------------------------------------------
   Single source of truth for the small, brand-wide visual rules
   that keep the app feeling like one product across login,
   dashboard, classroom, heist, writing, exam, and teacher views.

   Rule for additions: only put things here that apply EVERYWHERE
   and don't conflict with per-page custom styles. If a rule only
   makes sense on the dashboard, it belongs in extra.css, not here.
   ════════════════════════════════════════════════════════════════ */

/* ── Number inputs ──────────────────────────────────────────────
   The default browser number-spinner buttons look like Windows 98
   next to the rest of our dark-glass UI. Hide them everywhere.
   The input is still numeric - only the visual chrome is gone.
   • Webkit (Chrome / Edge / Safari) needs ::-webkit-*-spin-button
     plus appearance:none on the input itself.
   • Firefox needs -moz-appearance:textfield on the input.            */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* ── Search inputs ─────────────────────────────────────────────
   Hide Webkit's native clear-X (looks bolted on, not branded).
   Most pages style the surrounding wrapper; the X is the only
   uncontrolled bit. Safe everywhere - no current search input
   needs the native clear button. */
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

/* ── Focus-visible default ─────────────────────────────────────
   When a control has NO custom :focus style, browsers fall back
   to a low-contrast OS outline that's invisible against the dark
   backgrounds. Give all unstyled focusable elements a faint indigo
   ring so they're always visible without overriding the strong
   per-component focus rings (those use box-shadow or a custom
   outline-color and take precedence). */
:focus-visible {
  outline: 2px solid rgba(37, 99, 235, 0.55);
  outline-offset: 2px;
}

/* ── Selection color ───────────────────────────────────────────
   Default text-selection blue clashes with everything; pick a
   subtle indigo that matches the app accent. */
::selection {
  background: rgba(37, 99, 235, 0.42);
  color: #ffffff;
}

/* ── Checkboxes & radios ───────────────────────────────────────
   Recolor every native checkbox/radio to the brand indigo so they
   match across the whole app (login, dashboards, forms, admin).
   `accent-color` keeps the native control's behaviour + sizing -
   it only changes the tick/fill colour - so it CANNOT break the
   layout of any existing checkbox, and it correctly does nothing
   to the assignment student-picker (which hides its native input
   and draws its own custom box). Pointer cursor + a soft indigo
   focus ring complete the polish. */
input[type="checkbox"],
input[type="radio"] {
  accent-color: #2563eb;
  cursor: pointer;
}
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  outline: 2px solid rgba(37, 99, 235, 0.6);
  outline-offset: 2px;
  border-radius: 3px;
}
/* The <label> beside a checkbox/radio should feel clickable too. */
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
  cursor: pointer;
}

/* ── Mobile accordion performance ──────────────────────────────
   The exam / assignment accordions animate max-height (a layout
   property that reflows every frame) inside backdrop-filter: blur
   cards, so the blur re-renders over the moving area on every
   frame. On phones that combination is visibly laggy. On small
   screens we make the height change ~instant (keeping a soft
   opacity + padding fade) and drop the per-child stagger, so
   opening/closing is snappy with no jank. Desktop keeps the full
   animation. !important is required to beat the more-specific
   per-file `.is-collapsed` transitions. */
/* GATED ON TOUCH, NOT ON WIDTH.
   This block used to be `@media (max-width: 768px)` alone, so it covered phones
   and nothing else. A Galaxy Tab S9 FE+ reports well over 768px, so a tablet got
   the FULL desktop treatment - live backdrop blur plus max-height accordion
   animations - while pushing far more pixels than any phone on a mid-range GPU.
   That is why the tablet felt heavier than the phones this was tuned for.
   `(hover: none) and (pointer: coarse)` is the standard touch-device test: true
   on phones and tablets, false on desktop and on a laptop with a touchscreen
   (whose PRIMARY pointer still hovers), so the frosted look is kept exactly
   where there is hardware to pay for it. */
@media (max-width: 768px), (hover: none) and (pointer: coarse) {
  .rd-section-body, .rd-q-body,
  .exam-section-body, .exam-item-body, .exam-q-body,
  .asg-acc-detail {
    transition: max-height 1ms linear,
                padding 0.16s ease,
                opacity 0.16s ease !important;
  }
  .rd-section-body > *, .rd-q-body > *,
  .exam-section-body > *, .exam-item-body > *, .exam-q-body > * {
    transition: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* The other big phone cost is backdrop-filter: blur. It re-renders on every
     scroll frame (worst on sticky toolbars) and over the fade above. Every
     surface in this app sits on a near-opaque background (0.72 to 0.95 alpha),
     so the blur is decorative here; dropping it on touch devices removes the
     remaining jank with almost no visual change. Desktop keeps the frosted look. */
  * {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
}

/* ============================================================
   ONE DROPDOWN, EVERYWHERE  (.ll-select)
   ------------------------------------------------------------
   The app has a single dropdown look, modelled on the Students
   filter bar. Any <select> that carries `ll-select` gets it, on
   every page - login, student hub, teacher dashboard, placement.
   Enforced by y/tools/ui-sanity.js, which FAILS the build when a
   <select> ships without this class (allowlist for game themes).

   Kept last in this file and scoped to the class only, so a page
   can still override a single property (e.g. a narrow width) by
   adding its own class after this one.
   ============================================================ */
select.ll-select {
  padding: 10px 16px;
  background: var(--bg-item, rgba(255, 255, 255, 0.04));
  border: 1px solid var(--border-color, rgba(59, 130, 246, 0.2));
  border-radius: 8px;
  color: var(--text-primary, #f8fafc);
  font-family: inherit;
  font-size: 0.875rem;
  cursor: pointer;
  outline: none;
}

select.ll-select:focus {
  border-color: var(--accent-primary, #3b82f6);
}

select.ll-select:disabled {
  opacity: 0.55;
  cursor: default;
}

/* ============================================================
   LIGHT MODE - generated by tools/light-coverage.js findings
   Each rule mirrors a real one above and flips only its SURFACE,
   keeping the original alpha so translucent panels stay translucent
   and nothing about the layering changes. Dark is untouched.
   ============================================================ */
.light-mode ::selection {
  color: var(--text-primary, #0f172a);
}

/* ============================================================
   DIALOGS - one shape and one motion for every confirmation box
   ------------------------------------------------------------
   There were four separate implementations, agreeing on almost nothing:

     student logout   radius 24px  padding 36px 44px  max 420px  bouncy 0.3s
     teacher logout   radius 16px  padding 28px       max 500px  NO animation
     AppDialog        radius 18px  padding 24/24/20   max 420px  NO animation
     reading exam     radius 14px  padding 24/22/18   max 420px  slide 0.2s

   Four radii, four paddings, two widths, and two of them appeared with no
   entry animation at all, which is why some glided in and others snapped.

   This layer owns the SHAPE and the MOTION only. Colour, border and
   background stay with each dialog, so the dark exam sheets stay dark and the
   white logout card stays white. brand.css is loaded by all 39 pages, and by
   every page that uses app-dialog.js, so this reaches all of them.
   ============================================================ */
:root {
  --ll-dlg-radius: 18px;
  --ll-dlg-pad: 28px 28px 22px;
  --ll-dlg-width: min(420px, calc(100vw - 32px));
  --ll-dlg-dur: 0.3s;
  /* The student logout's curve. It overshoots slightly, which is the bounce. */
  --ll-dlg-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ll-dlg-from: scale(0.9) translateY(20px);
}

@keyframes llDialogIn {
  from { transform: scale(0.9) translateY(20px); opacity: 0; }
  to   { transform: scale(1) translateY(0);      opacity: 1; }
}

/* Class doubled on purpose. js/app-dialog.js injects its stylesheet at runtime,
   so its rules land after this file and would win a tie on equal specificity.
   Applied by an added marker class rather than to .modal-box or .modal-content
   directly, because .modal-box is ALSO the big assignment and writing modals,
   which set their own width inline - a blanket rule would crush them to 420px. */
.ll-dialog.ll-dialog {
  border-radius: var(--ll-dlg-radius);
  padding: var(--ll-dlg-pad);
  width: var(--ll-dlg-width);
  max-width: var(--ll-dlg-width);
  box-sizing: border-box;
}

/* Two mechanisms, deliberately, because the dialogs arrive two different ways.
   Both use the same curve, duration and starting transform, so they move
   identically on screen.

   BUILT ON OPEN (AppDialog, the reading exam) - a keyframe, since the element
   does not exist until the moment it should animate. */
.ll-dialog.ll-dialog-pop {
  animation: llDialogIn var(--ll-dlg-dur) var(--ll-dlg-ease) both;
}

/* ALREADY IN THE MARKUP, revealed by .active (both logout modals) - a
   transition. A keyframe here would fire on page load, spending itself while
   the modal is still hidden, and the modal would then just appear. */
.modal-overlay .ll-dialog.ll-dialog-toggle {
  transform: var(--ll-dlg-from);
  transition: transform var(--ll-dlg-dur) var(--ll-dlg-ease);
}
.modal-overlay.active .ll-dialog.ll-dialog-toggle,
.modal-overlay.open .ll-dialog.ll-dialog-toggle {
  transform: scale(1) translateY(0);
}

/* The backdrop. Previously three different treatments: two blurs of different
   radius and one with none, one fading and one snapping via display:none.
   Every open/close in the app goes through the .active class (92 call sites,
   none of them touching style.display), so driving this with opacity and
   visibility is safe and gives the same fade everywhere.

   pointer-events and the blur are BOTH gated on .active. That is not tidiness:
   a backdrop-filter layer keeps swallowing clicks in Chromium even at zero
   opacity, which is what the original CRITICAL note in the student stylesheet
   was about. Keep them gated. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--ll-dlg-dur) ease, visibility var(--ll-dlg-dur) ease;
}
/* BOTH reveal classes. The app uses two and always has: .active on the
   dashboards and the classroom, .open on the index/login modals. Because the
   base rule above hides with `visibility`, a reveal class that does not restore
   it leaves the dialog permanently invisible - and neither the classroom nor
   the index stylesheet sets visibility, since they only ever faded with
   opacity. Listing .active alone here would have blanked every login modal in
   the app. If a third reveal class ever appears, it must be added here too. */
.modal-overlay.active,
.modal-overlay.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* The round icon above the title. The student logout had 72px, the reading
   exam 52px, the teacher logout a bare 3em emoji and AppDialog none at all. */
.ll-dialog-icon {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 16px;
  font-size: 26px;
  line-height: 1;
}

/* The two buttons, one size everywhere. Same story as the icon above: three
   separate `.modal-btn` rules existed (student 14px/32px, teacher 10px/20px,
   classroom 10px/22px), so the SAME logout dialog came out 37px wider and
   16px taller on one page than another.

   Worse, inside a single dialog the two buttons did not match each other:
   only the classroom rule set a min-width, so everywhere else each button was
   the width of its own text and "Stay" ended up 25px narrower than "Log out".

   `flex: 1 1 0` is the part that actually settles it. With the `1 1 auto` used
   before, each button starts from its own text width and grows from there, so
   unequal labels can only ever give unequal buttons. A zero basis makes the
   starting width identical, so the two always finish the same.

   GEOMETRY ONLY. Colour stays with the page, so cancel / confirm / delete keep
   their own meaning; this decides size and shape, nothing else. Matches
   .appdlg-btn in js/app-dialog.js, so the built-on-open dialogs agree too. */
.ll-dialog .modal-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}

.ll-dialog .modal-buttons .modal-btn {
  flex: 1 1 0;
  min-width: 110px;
  max-width: 200px;
  box-sizing: border-box;
  /* min-height, not a border-width, is what levels the heights. The three
     pages use different border widths (student 2px, classroom 1.5px, teacher
     none) and those carry the colour that tells cancel from confirm, so
     normalising the border here would either strip the student outline or
     paint a new one on the teacher button. A floor on the height leaves every
     border exactly as it is and still lands all three on 48px - which is also
     a proper touch target. A button centres its own label vertically, so
     nothing else is needed. */
  min-height: 48px;
  padding: 12px 22px;
  border-radius: 10px;
  font-family: inherit;
  /* rem, NOT em. An em is a share of whatever the parent card happens to be
     set to, so the same 0.95 gave a different pixel size on each dashboard -
     which is the bug this rule was added to fix. A rem is the same everywhere.
     Before this rule the gap was wider still: the student button said
     `font-size: 1em` (16px, Segoe) and the teacher button set no font at all,
     so it fell back to the browser default 13.3px Arial. */
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
}

/* Stacked on a phone, where side by side leaves each one too narrow to hit.
   This has to be restated here: the per-page rules that used to do it are
   plain `.modal-btn`, which this block now outranks. */
@media (max-width: 480px) {
  .ll-dialog .modal-buttons .modal-btn {
    flex: 1 1 100%;
    max-width: none;
  }
}

/* THE CARD ITSELF, and the three things inside it above the buttons.
   Equal buttons were only part of the answer: the same logout dialog was still
   346.6px tall on the student dashboard and 275px on the teacher one, because
   every piece above the buttons was sized separately.

     icon     72px (student .modal-icon) vs 56px
     title    h2 at 24px vs h3 at 18.7px
     message  16.8px vs 16px

   Each selector below is (0,2,0) or better so it outranks the per-page
   (0,1,0) rules - .modal-icon{width:72px} in student/css/styles.css and the
   rest - whatever order the stylesheets load in.

   EVERY SELECTOR TARGETS A CLASS, NEVER A BARE h2/h3/p. This matters more than
   it looks. `.ll-dialog` is not the logout dialog - it is the shared shape and
   motion layer, and FOUR different dialogs wear it:

     .modal-content / .modal-box   the logout dialogs        <- these, and only these
     .ph-panel                     the profile hub panels    (js/profile-hub.js)
     .rd-confirm                   the reading exam confirm  (student/css/reading-exam.css)
     .appdlg-card                  AppDialog                 (js/app-dialog.js)

   The first version of this block used `.ll-dialog > h3` and `.ll-dialog > p`,
   which reached into all four. It would have retyped the profile hub's panel
   headings and their 4px sub-line, and flattened the reading exam confirm -
   whose type is deliberately smaller, and whose phone @media override would
   have become dead code, since a media query adds no specificity. Matching on
   .modal-title / .modal-message instead cannot reach any of them: neither
   profile-hub.js nor reading-exam.js uses those class names at all.

   That is also why the teacher dashboard's bare <h3>/<p> were given the
   .modal-title / .modal-message classes rather than being matched as elements.

   The class is still DOUBLED, the idiom this file already uses. A single
   .ll-dialog plus a class scores (0,2,0), which only TIES a per-page rule - and
   on a tie the later sheet wins, which is never this one, because brand.css is
   loaded first on purpose. */
.ll-dialog.ll-dialog .ll-dialog-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto 16px;
  font-size: 26px;
  line-height: 1;
}

.ll-dialog.ll-dialog > .modal-title {
  margin: 0 0 12px;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
}

.ll-dialog.ll-dialog > .modal-message {
  margin: 0 0 24px;
  font-size: 1rem;
  line-height: 1.6;
}

/* Border WIDTH only. The colour is what tells the cards apart on purpose
   (student blue, teacher neutral), and it is set by the same shorthand that
   sets the width, so this has to override one without touching the other.
   Pinned to the two card classes rather than .ll-dialog, so the profile hub
   panel and the reading exam confirm keep their own 1px. */
.ll-dialog.modal-content,
.ll-dialog.modal-box {
  border-width: 1.5px;
}

@media (prefers-reduced-motion: reduce) {
  .ll-dialog.ll-dialog-pop,
  .modal-overlay .ll-dialog.ll-dialog-toggle,
  .modal-overlay { animation: none !important; transition-duration: 0.01ms !important; }
  .modal-overlay .ll-dialog.ll-dialog-toggle { transform: none; }
}

/* ============================================================
   DATE AND TIME INPUTS - make the picker button findable
   ------------------------------------------------------------
   The native calendar button is a small low-contrast glyph tucked against the
   right edge. On a white card it is close to invisible, which is why the
   writing form's Deadline field looked like a plain text box.

   We draw our OWN icon instead of nudging the browser's.

   A first attempt only set opacity and then inverted the glyph on
   `body:not(.light-mode)`. That was worse, not better: the teacher dashboard is
   visually white but carries no `.light-mode` class, so the rule inverted a
   dark icon to WHITE ON WHITE. Theme detection by class is not reliable here,
   because "light" is expressed three different ways across this app.

   Supplying a background-image replaces the default glyph outright, so the
   result no longer depends on the theme, the colour-scheme or the platform.
   Brand blue reads against both the white cards and the dark navy ones.

   These inputs are bare `<input type="date">` with no class, so styling them
   here reaches every one in the app rather than needing a class in a dozen
   places. Firefox has no such pseudo-element and draws its own control; nothing
   to do there, noted so nobody hunts for why this is webkit-only.
   ============================================================ */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="month"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
  opacity: 1;
  cursor: pointer;
  width: 18px;
  height: 18px;
  padding: 0;
  margin-left: 6px;
  border-radius: 4px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 18px 18px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
  transition: background-color 0.15s ease;
}
input[type="date"]::-webkit-calendar-picker-indicator:hover,
input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover,
input[type="month"]::-webkit-calendar-picker-indicator:hover,
input[type="time"]::-webkit-calendar-picker-indicator:hover {
  background-color: rgba(59, 130, 246, 0.16);
}

/* ============================================================
   WRONG-ANSWER FEEDBACK, APP-WIDE
   ------------------------------------------------------------
   Two vocabulary modes already shook (.choice-btn.incorrect and
   .match-item.incorrect, each with their own `animation: shake`), but the
   classroom games, grammar and the policy course only changed colour. So a
   wrong answer felt different depending on which part of the app you were in.

   This is the floor, not an override: it is a single-class selector, so every
   existing two-class rule like `.choice-btn.incorrect` still wins and keeps
   its own animation. It only reaches elements that had none.

   Named ll-shake, NOT shake: `@keyframes shake` already exists twice (in
   student/css/styles.css and inline in app.html) and a third definition of
   that name would silently replace theirs wherever both loaded.

   `.error` is deliberately NOT included. Its only scripted use is a SAVE
   BUTTON in assignments/js/teacher-assignments.js, and six unrelated CSS
   rules use it for messages - shaking those would be wrong.
   ============================================================ */
@keyframes ll-shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-7px); }
  40%      { transform: translateX(7px); }
  60%      { transform: translateX(-4px); }
  80%      { transform: translateX(4px); }
}

.is-wrong,
.incorrect {
  animation: ll-shake 0.4s ease-in-out;
}

/* Feedback TEXT is told, not shaken. Shaking a sentence reads as a glitch;
   shaking the option you actually tapped reads as an answer. */
.aiq-feedback.is-wrong,
.feedback.incorrect,
.feedback-msg.incorrect {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .is-wrong,
  .incorrect {
    animation: none;
  }
}
