/* ============================================================
   AOE Centre — aoe-app.css   (build 0905n)
   ------------------------------------------------------------
   Everything this build changes about how a screen LOOKS lives
   here, so it can be reviewed in one file and removed in one
   line if any of it is wrong.

   Load order:  portal.css / style.css
             -> aoe-theme.css
             -> aoe-theme-vars.css
             -> THIS FILE

   Rules followed (AOEDESIGNSYSTEM.md):
   - every colour is a token; no raw hex in a component rule
     except the pinned lights on dark bands, which is what the
     rest of the site already does
   - nothing rounded but the two switches and 1-2px on buttons
   - 1px borders in --rule; 3px only on ONE edge, as an accent
   - Marcellus on titles and figures, Plex Mono uppercase for
     labels only, Karla for sentences
   - no shadow in dark mode
   ============================================================ */


/* ============================================================
   0. SURFACES — WHY NOTHING LOOKED LIKE A CARD
   ------------------------------------------------------------
   Measured on the live build, in dark mode:

     the page   #211E19
     the band   #2B2721   <- section.band-panel
     a card     #2B2721   <- .card

   portal.css line 78 gives `.band-panel` background:var(--panel),
   and line 731 gives `.card` background:var(--panel). They are
   THE SAME COLOUR. Every card on this site is drawn on a ground
   identical to itself, separated by one hairline at 1.30 contrast.
   That is why the sign-in box, the portal chooser and the
   placement-test panel all read as one flat sheet.

   No amount of palette tuning fixes that, because the numbers
   were never different. The band goes back to being the page,
   and then the card has something to sit on.

   With that fixed, the palette is widened so the step is visible
   on a phone at low brightness:

              was                    now         card : ground
     light    page  #F5F2EA          #EBE6DA          1.22
              card  #FFFDF7          #FFFDF7   (kept: the warm white
                                                is the brand)
              rule  #E2DBCB          #DFD7C6          1.41 on a card
     dark     page  #211E19          #171410          1.24
              band  #2B2721          #211E19          1.12 under a card
              card  #2B2721          #2B2721   (kept)
              rule  #3E3931          #453F35          1.42 on a card

   Three steps in both themes — page, band, card — instead of one
   colour used for all three. Text contrast goes UP everywhere,
   never down: cream on the new dark page is 15.9, ink on the new
   light page is 12.7.

   Light gets a soft shadow under cards as well. Dark does not:
   a shadow on a dark ground reads as blur, not lift, which is
   why AOEDESIGNSYSTEM.md forbids it.
   ============================================================ */

:root {
  --cream: #EBE6DA;        /* the page, now a step below a card  */
  --rule:  #DFD7C6;        /* 1.41 against the card it sits on   */
  --taupe-dark: #6C6558;   /* dim text: 4.63 on the deeper page  */
  --gold-deep: #71611E;    /* the deeper page costs the old gold
                              #7E6C2B 4.15; this reads 4.9 on the
                              page and 6.0 on a card              */
}

html[data-theme="dark"] {
  --cream: #171410;        /* the page  */
  --taupe: #211E19;        /* masthead, hero band, footer */
  --taupe-deep: #211E19;
  --header: #211E19;       /* aoe-theme.css paints the bands from this */
  --rule:  #453F35;
}
html[data-theme="dark"] .rail { background: #211E19; }

/* THE ONE THAT MATTERS: the band behind every card was the card's
   own colour. portal.css line 78 sets it from --panel in light, and
   aoe-theme.css line 228 sets it from --surface in dark — the same
   value .card uses in each. Both are corrected here. */
.band-panel { background: var(--cream); }
html[data-theme="dark"] .band-panel {
  background: var(--cream); border-top-color: var(--rule); border-bottom-color: var(--rule);
}

/* a card, lifted off the page */
.card, .panel, .setcard, .route, .teaser, .noticeboard, .update,
.fixbox, .boardbox, .way, .ptest-say {
  box-shadow: 0 1px 2px rgba(43, 39, 33, .05), 0 4px 14px rgba(43, 39, 33, .07);
}
html[data-theme="dark"] .card,
html[data-theme="dark"] .panel,
html[data-theme="dark"] .setcard,
html[data-theme="dark"] .route,
html[data-theme="dark"] .teaser,
html[data-theme="dark"] .noticeboard,
html[data-theme="dark"] .update,
html[data-theme="dark"] .fixbox,
html[data-theme="dark"] .boardbox,
html[data-theme="dark"] .way,
html[data-theme="dark"] .ptest-say { box-shadow: none; }

/* The footer and the hero heading already have dark rules in
   aoe-theme.css (lines 214-220); they are restated here only so
   the whole band family — masthead, hero, footer, rail — lands on
   one colour, #211E19, one step above the page. */
html[data-theme="dark"] footer { background: #211E19; }
html[data-theme="dark"] footer .script { color: var(--gold); }


/* ============================================================
   1. THE TOP OF THE APP ON A PHONE
   ------------------------------------------------------------
   The original fault: .masthead .wrap is a 72px flex row and its
   nav wraps. Once the bottom bar took Store / Play / Friends /
   Profile out of it, what was left — Sign out, EN/中文,
   LIGHT/DARK — still needed two lines, and the second line sat on
   the logo.

   0902i emptied the bar and put a gear there instead. That went
   too far: it left a 54px bar holding one small logo and one icon
   with 250px of nothing between them, and — worse — it hid the
   switches on enter.php and login.php, where a visitor has no
   profile page to find them on.

   0902i keeps the bar and uses it. The two switches ARE the
   buttons; the word links are what wrapped, and those are the
   ones that go. Everything they pointed at is one tap away in the
   bottom bar, and Sign out is on the profile page.

       [owl]                       [EN|中文] [LIGHT|DARK]

   Same on public pages and portal pages, signed in or not. No
   page is left without a way to change theme or language, which
   is the rule the design system already had.
   ============================================================ */

@media (max-width: 760px) {
  body.has-tabs .masthead .wrap {
    height: 54px; gap: 8px; flex-wrap: nowrap;
  }
  body.has-tabs .masthead .logo { height: 32px; }

  /* the row that holds the switches, pushed right, never wrapping */
  body.has-tabs .masthead nav {
    display: flex; flex-wrap: nowrap; align-items: center;
    gap: 8px; margin-left: auto; min-width: 0;
  }

  /* the word links are the part that overflowed. Store, Play,
     Friends and Profile are in the bottom bar; Sign out, Reset
     and the portal swap are on the profile page. */
  body.has-tabs .masthead nav a[data-in-tabs] { display: none; }
  /* whatever is NOT in the bottom bar stays in the header, and
     the row scrolls rather than wrapping onto the logo */
  body.has-tabs .masthead nav { overflow-x: auto; scrollbar-width: none; }
  body.has-tabs .masthead nav::-webkit-scrollbar { display: none; }
  body.has-tabs .masthead nav a {
    font-size: 13px; white-space: nowrap; padding-bottom: 0;
  }

  /* the two switches stay, at a size that fits a 390px phone */
  body.has-tabs .masthead .aoe-pill,
  body.has-tabs .masthead #aoe-lang { display: inline-flex; flex-shrink: 0; }
  body.has-tabs .masthead .aoe-pill button,
  body.has-tabs .masthead #aoe-lang button {
    font-size: 10px; letter-spacing: .08em; padding: 8px 11px; line-height: 1;
  }

  /* the hero under it is a full-height banner built for a
     desktop. On a phone it is the first thing between the
     student and their work. */
  body.has-tabs .hero-sub { padding: 26px 0 30px; }
  body.has-tabs .hero-sub h1 { font-size: 30px; margin-top: 10px; }
  body.has-tabs .hero-sub .lede { font-size: 15px; margin-top: 10px; }
  body.has-tabs .streak-row { gap: 14px 20px; margin-top: 18px; }
  body.has-tabs .streak .n { font-size: 26px; }
  body.has-tabs .streak .cap { font-size: 12.5px; }
}

/* The figures under the hero heading were set at 85% and 60%
   opacity on the taupe band, which measures 4.1 and 2.9 — the
   small print under each number was the least readable text in
   the portal. Made solid, at both sizes, in both modes. */
.streak .cap { color: #F5F2EA; }
.streak .cap small { color: #EEEAE0; }  /* #DCD7CB measured 3.9 on this band, #EAE6DC 4.49 */

/* An installed app gets the same treatment at any width — that is
   what makes it read as an app rather than a page in a browser. */
@media (display-mode: standalone) {
  body.has-tabs .masthead .wrap { height: 54px; flex-wrap: nowrap; }
  body.has-tabs .masthead .logo { height: 32px; }
  body.has-tabs .masthead nav a[data-in-tabs] { display: none; }
  body.has-tabs .masthead nav {
    display: flex; flex-wrap: nowrap; align-items: center; gap: 8px; margin-left: auto;
  }
}


/* ============================================================
   2. SETTINGS, ON THE PROFILE PAGE
   Where the three controls went. Also the only place a student
   can reset the app, which is the whole point of it existing.
   ============================================================ */

.setgrid { display: grid; gap: 0; margin: 0 0 4px; }
.setrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 15px 0; border-bottom: 1px solid var(--rule);
  flex-wrap: wrap;
}
.setrow:last-of-type { border-bottom: 0; }
.setrow .setname { font-size: 15px; margin: 0; }
.setrow .setwhat {
  display: block; font-size: 13px; line-height: 1.55; color: var(--taupe-dark); margin-top: 3px;
}
.setrow .setctl { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* the switches sit on a card here, not on a coloured band, so
   they take the card treatment rather than the on-band one */
.setctl .aoe-pill,
.setctl #aoe-lang { background: var(--panel); border-color: var(--taupe-light); }
.setctl .aoe-pill button,
.setctl #aoe-lang button { color: var(--taupe-deep); }   /* --taupe-dark measured 4.0 on a card */
.setctl .aoe-pill button.on,
.setctl #aoe-lang button.on { background: var(--gold); color: #2B2721; }
html[data-theme="dark"] .setctl .aoe-pill,
html[data-theme="dark"] .setctl #aoe-lang { border-color: var(--rule); }
html[data-theme="dark"] .setctl .aoe-pill button,
html[data-theme="dark"] .setctl #aoe-lang button { color: #D8D2C6; }
html[data-theme="dark"] .setctl .aoe-pill button.on,
html[data-theme="dark"] .setctl #aoe-lang button.on { background: var(--gold); color: #211E19; }

.setfoot {
  margin: 18px 0 0; font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--taupe-deep);
}
html[data-theme="dark"] .setfoot { color: var(--taupe-dark); }


/* ============================================================
   3. RECENT ESSAY MARKS
   ------------------------------------------------------------
   It was a five-column table. table.mini pins the first column
   to 150px, so on a 390px phone four columns shared what was
   left and every one of them broke a word per line.

   A table is the wrong instrument: these are four records, not
   a grid to compare down. One block each, reading in the order
   a student wants it — what paper, when and how long, whether a
   teacher has marked it.
   ============================================================ */

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

.erow {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 16px;
  align-items: start;
  padding: 16px 0;
  border-bottom: 1px solid var(--rule);
}
.erow:last-child { border-bottom: 0; }
.erow:first-child { padding-top: 4px; }

.epaper {
  grid-column: 1; font-family: "Marcellus", Georgia, serif;
  font-size: 17px; line-height: 1.35; color: var(--ink);
}
.emeta {
  grid-column: 1; font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--taupe-dark); line-height: 1.5;
}
.estate { grid-column: 1; font-size: 14px; line-height: 1.5; margin-top: 3px; }
.estate .emark { font-family: "Marcellus", Georgia, serif; font-size: 19px; color: var(--gold-deep); }
.estate .ewait { color: var(--taupe-dark); }

.eopen {
  grid-column: 2; grid-row: 1 / span 3; align-self: center;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  text-decoration: none; color: var(--gold-deep);
  border: 1px solid var(--rule); padding: 10px 14px; border-radius: 2px;
  white-space: nowrap;
}
.eopen:hover { border-color: var(--gold-deep); }
html[data-theme="dark"] .eopen { border-color: var(--rule); color: var(--gold); }
html[data-theme="dark"] .eopen:hover { border-color: var(--gold); }

@media (max-width: 460px) {
  .erow { grid-template-columns: 1fr; }
  .eopen { grid-column: 1; grid-row: auto; justify-self: start; margin-top: 10px; }
}


/* ============================================================
   4. YOUR ACCOUNT — FOLDED
   ------------------------------------------------------------
   Seven subjects, six identity rows and five sign-in records
   were all printed at once, so the card was longer than the
   phone and the one line that matters — the next renewal — was
   somewhere in the middle of it.

   Folded. The summary carries the answer; opening it shows the
   working. A fold that is hiding something urgent opens itself
   (see aoe-app.js), so nothing important is ever behind a tap.
   ============================================================ */

.fold { border-top: 1px solid var(--rule); margin-top: 22px; }
.fold > summary {
  list-style: none; cursor: pointer;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;   /* the arrow keeps its own
                                                   column, so a long summary
                                                   can never push it onto a
                                                   line of its own */
  column-gap: 12px; row-gap: 3px;
  padding: 15px 0 14px;
  -webkit-tap-highlight-color: transparent;
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::marker { content: ""; }

.fold .foldname {
  grid-column: 1; grid-row: 1;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--taupe-dark);
}
/* one line, cut with an ellipsis when it is long — the whole point of
   a summary is that the detail is one tap away */
.fold .foldsum {
  grid-column: 1; grid-row: 2;
  font-size: 14.5px; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fold[open] .foldsum { white-space: normal; }
.fold .foldsum b { font-weight: 400; }
.fold .foldarrow {
  grid-column: 2; grid-row: 1 / span 2; align-self: center;
  color: var(--taupe-dark); font-size: 12px; line-height: 1;
}
.fold[open] .foldarrow { transform: rotate(180deg); }
.fold .foldarrow { transition: transform .16s ease; display: inline-block; }
@media (prefers-reduced-motion: reduce) { .fold .foldarrow { transition: none; } }
.fold > summary:hover .foldsum { color: var(--gold-deep); }
.fold > *:not(summary) { padding-bottom: 16px; }

/* the renewals list, once opened */
.renew { list-style: none; margin: 0; padding: 0; }
.renewrow {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--rule); font-size: 14px; flex-wrap: wrap;
}
.renewrow:last-child { border-bottom: 0; }
.renewrow .rn { flex: 1 1 auto; min-width: 0; }
.renewrow .rd { color: var(--taupe-dark); font-size: 13px; white-space: nowrap; }
.renewrow .rs { flex-shrink: 0; }
.renewrow .rs .dim { font-size: 13px; }

/* A phone had 150px of a 390px screen going to the label column of
   every .mini table. Narrowed everywhere; and the account's identity
   table — the one that really is label-and-value — stacks properly,
   marked .keyval so no other .mini table is restyled by accident. */
@media (max-width: 560px) {
  table.mini td:first-child { width: 34%; }

  table.mini.keyval tr {
    display: grid; grid-template-columns: 1fr;
    padding: 10px 0; border-bottom: 1px solid var(--rule);
  }
  table.mini.keyval td { border-bottom: 0; padding: 0; width: auto; }
  table.mini.keyval td:first-child {
    font-family: "IBM Plex Mono", "Consolas", monospace;
    font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
    margin-bottom: 3px; width: auto;
  }
  table.mini.keyval td.acts { text-align: left; margin-top: 6px; }
}


/* ============================================================
   5. THE STORE CHIPS
   ------------------------------------------------------------
   portal.css gives .chips a  color:var(--taupe-dark) — a mid
   taupe. The store's chips sit on the taupe hero band. Mid
   taupe on mid taupe measured 1.4:1, which is why "For
   teaching" could not be read at all: it was not faint, it was
   the same colour as what was behind it.

   On a band the chips take the band's own light, the way every
   other on-band control on this site does.
   ============================================================ */

.hero .chips a,
.top .chips a,
.band-taupe .chips a {
  color: #F5F2EA;
  border-color: rgba(245, 242, 234, .55);
}
.hero .chips a:hover,
.top .chips a:hover,
.band-taupe .chips a:hover {
  border-color: #EFE0A8; color: #EFE0A8;
}
.hero .chips a.on,
.top .chips a.on,
.band-taupe .chips a.on {
  background: var(--gold); border-color: var(--gold); color: #2B2721;
}

/* a chip is a control, so it gets a control's tap target */
.chips a { padding: 10px 16px; font-size: 13px; line-height: 1; }
.storeswitch { gap: 8px; margin-top: 22px; }


/* ============================================================
   6. OPEN A ROOM
   ------------------------------------------------------------
   Six dropdowns in a 1fr 1fr grid with no phone breakpoint, so
   at 390px each field had about 150px and every option label
   was cut mid-word — "Everything i", "10 points fo", "No timer
   — a". Under them, five lines of small print nobody reads.

   One field per line on a phone. The six are grouped into the
   two questions actually being answered — what are we playing,
   and how does it score — so it reads as two decisions instead
   of six. The small print folds away.
   ============================================================ */

@media (max-width: 700px) {
  .row { grid-template-columns: 1fr; gap: 14px; margin-bottom: 14px; }
}

/* .row sets display:grid, which beats the browser's own
   [hidden]{display:none}. Without this, "Which class" would show
   even when it is hidden — the same trap that kept the homepage
   directory open until 31p. */
.row[hidden], .field[hidden], .fsec[hidden] { display: none !important; }

/* a field, on any screen */
.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.field > label { color: var(--gold-deep); }
.card label { color: var(--gold-deep); }

.card select,
.card input[type="text"],
.card input[type="number"],
.card input[type="email"],
.card input[type="tel"] {
  width: 100%; min-height: 46px; font-size: 15px;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--taupe-light); border-radius: 2px;
  padding: 11px 38px 11px 13px;
}
.card select {
  -webkit-appearance: none; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 17px center, right 12px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  text-overflow: ellipsis;
}
html[data-theme="dark"] .card select,
html[data-theme="dark"] .card input[type="text"],
html[data-theme="dark"] .card input[type="number"],
html[data-theme="dark"] .card input[type="email"],
html[data-theme="dark"] .card input[type="tel"] {
  background: #332F28; color: var(--ink); border-color: #5C554C;
}

/* the two halves of the form */
.fsec { margin: 0 0 4px; }
.fsec + .fsec { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--rule); }
.fsec > .fsechead {
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--taupe-dark); margin: 0 0 14px;
}

.formgo { margin-top: 24px; }
.formgo .btn { min-height: 50px; padding: 0 30px; font-size: 15px; }
@media (max-width: 560px) { .formgo .btn { width: 100%; } }

/* the small print, folded */
.smallprint { margin-top: 18px; }
.smallprint > summary {
  list-style: none; cursor: pointer;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--gold-deep); padding: 8px 0;
  -webkit-tap-highlight-color: transparent;
}
.smallprint > summary::-webkit-details-marker { display: none; }
.smallprint > summary::marker { content: ""; }
.smallprint p { font-size: 13.5px; line-height: 1.75; color: var(--taupe-dark); margin: 4px 0 0; }

/* the join-a-room box, which shared the cramped grid */
@media (max-width: 560px) {
  .redeem { display: flex; flex-wrap: wrap; gap: 10px; }
  .redeem .codebox, .redeem select { flex: 1 1 100%; }
  .redeem .btn { flex: 1 1 100%; min-height: 48px; }
}


/* ============================================================
   7. SKELETON LOADERS
   ------------------------------------------------------------
   Used ONLY where the screen genuinely waits on the network —
   the game room before its first poll lands, and the topic list
   while it is being filled. Every other page here is rendered
   by PHP and arrives complete, and a skeleton on something that
   is already there is a lie about how fast the page is.
   ============================================================ */

.sk { display: block; background: var(--rule); border-radius: 2px; position: relative; overflow: hidden; }
.sk + .sk { margin-top: 10px; }
.sk-line { height: 13px; }
.sk-line.w80 { width: 80%; }
.sk-line.w60 { width: 60%; }
.sk-line.w40 { width: 40%; }
.sk-title { height: 22px; width: 55%; margin-bottom: 14px; }
.sk-block { height: 52px; }

.sk::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(245,242,234,.55), transparent);
  transform: translateX(-100%);
  animation: sksweep 1.25s ease-in-out infinite;
}
html[data-theme="dark"] .sk::after {
  background: linear-gradient(90deg, transparent, rgba(245,242,234,.09), transparent);
}
@keyframes sksweep { to { transform: translateX(100%); } }

@media (prefers-reduced-motion: reduce) {
  .sk::after { animation: none; }
}

.skwrap[hidden] { display: none !important; }


/* ============================================================
   8. TOOLTIPS ON ICON-ONLY BUTTONS
   ------------------------------------------------------------
   An icon with no word beside it is a guess. Every icon-only
   control on the site carries data-tip; this draws it on hover,
   on keyboard focus, and on a long press on a phone, where
   there is no hover to rely on.

   It is a label, not a help system: one short phrase, and it
   never carries anything a student has to read to continue.
   ============================================================ */

[data-tip] { position: relative; }

.aoe-tip {
  position: fixed; z-index: 200;
  background: var(--ink); color: var(--cream);
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  padding: 7px 10px; border-radius: 2px;
  pointer-events: none; white-space: nowrap;
  opacity: 0; transform: translateY(3px);
  transition: opacity .12s ease, transform .12s ease;
}
.aoe-tip.on { opacity: 1; transform: none; }
html[data-theme="dark"] .aoe-tip { background: #4A443B; color: #F5F2EA; }
@media (prefers-reduced-motion: reduce) { .aoe-tip { transition: none; } }


/* ============================================================
   9. A TAP THAT ANSWERS AT ONCE
   ------------------------------------------------------------
   On a phone on 4G there is a third of a second between a tap
   and anything happening, and in that gap a student taps again.
   Every button now marks itself the instant it is pressed, and
   a form that has been submitted will not submit twice.

   Nothing here pretends the work is DONE — that would be a lie
   when the server can still refuse. It only says "heard you".
   ============================================================ */

.btn, .pk-btn, .tinybtn, .chips a, .tabs a, .eopen, .aoe-tabs a {
  transition: transform .08s ease, opacity .08s ease;
}
.is-pressed { transform: scale(.975); opacity: .85; }
@media (prefers-reduced-motion: reduce) {
  .btn, .pk-btn, .tinybtn, .chips a, .tabs a, .eopen, .aoe-tabs a { transition: none; }
  .is-pressed { transform: none; }
}

/* a form that is on its way to the server */
.is-busy { pointer-events: none; opacity: .72; }
.is-busy::after {
  content: "…"; margin-left: 6px; letter-spacing: .1em;
}

/* every control on a phone gets a real tap target */
@media (max-width: 760px) {
  .btn { min-height: 46px; display: inline-flex; align-items: center; justify-content: center; }
  .btn.tiny { min-height: 38px; }
}


/* ============================================================
   10. THE RESET NOTE
   The app renews itself silently and says nothing about it (see
   aoe-app.js). The one time it does speak is when a student has
   pressed "Reset the app" and the screen would otherwise sit
   there looking broken while the caches are cleared.
   ============================================================ */

.aoe-fresh {
  position: fixed; left: 12px; right: 12px; bottom: 74px; z-index: 90;
  background: var(--panel); border: 1px solid var(--rule);
  border-left: 3px solid var(--gold-deep);
  padding: 12px 14px; font-size: 13.5px; line-height: 1.5;
  display: flex; align-items: center; gap: 12px;
  box-shadow: 0 2px 14px rgba(43, 39, 33, .16);
}
html[data-theme="dark"] .aoe-fresh { box-shadow: none; border-left-color: var(--gold); }
.aoe-fresh button {
  margin-left: auto; flex-shrink: 0;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
  background: var(--gold); color: #2B2721; border: 0; border-radius: 2px;
  padding: 10px 14px; cursor: pointer;
}
@media (min-width: 761px) { .aoe-fresh { left: auto; right: 18px; bottom: 18px; max-width: 380px; } }


/* ============================================================
   11. SMALL PRINT FOR SCREEN READERS ONLY
   The game room's "Loading…" line is now a skeleton, which is a
   picture. The words stay for anyone listening to the page.
   ============================================================ */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}


/* ============================================================
   12. THE HOMEPAGE — THE LIGHT DIRECTORY
   ------------------------------------------------------------
   Two separate faults, one in each theme.

   (a) IN DARK, the phone top bar came out CREAM with nothing
       written on it. style.css line 944 sets
       `.mbar{background:var(--ink)}` — and --ink is the BODY TEXT
       colour, which aoe-theme-vars flips to #F5F2EA in dark. So
       the bar turned pale, and aoe-theme-vars had already pinned
       its text light for the dark ground it was supposed to be.
       Light on light: the wordmark simply was not there.

   (b) THE RAIL WAS DARK IN LIGHT MODE. On a computer that is the
       1b design — a dark column down the left. On a phone the
       grid restacks and that column becomes the directory panel
       filling the screen, so light mode read as a dark page.

       0901c made it light below 900px only. Jason's call at
       0902i: light on the desktop too. The colours below are no
       longer inside a media query — in light mode the rail is a
       light panel at every width, and the only thing that stays
       phone-only is the scroll reset in section 14, which is
       layout rather than colour and MUST NOT reach the desktop
       column (it needs its own scrollbar there).

   Almost every rule inside the rail draws its colour from
   var(--cream), var(--ink-soft) and var(--gold-deep), so those
   three are re-pointed ON THE RAIL ITSELF and the children follow
   without being listed. Only the rules that wrote an rgba white
   straight into the stylesheet have to be named.
   ============================================================ */

/* (a) the phone top bar, in dark */
html[data-theme="dark"] .mbar { background: #211E19; }
html[data-theme="dark"] .mbar-name { color: #F5F2EA; }
html[data-theme="dark"] .mbar-menu { color: var(--gold); }

/* (b) the directory panel, in light, at every width */
html:not([data-theme="dark"]) .rail {
  background: var(--panel);

  /* re-point the three the rail's own rules read */
  --cream: #2B2721;        /* was the light text on a dark column */
  --ink-soft: #F3EFE4;     /* the row fill                        */
  --gold-deep: #7E6C2B;    /* aoe-theme-vars pins this bright
                              inside .rail for the dark column    */
}
/* a light column against a cream page needs an edge; a light panel
   above the content needs a floor */
@media (min-width: 901px) {
  html:not([data-theme="dark"]) .rail { border-right: 1px solid var(--rule); }
}
@media (max-width: 900px) {
  html:not([data-theme="dark"]) .rail { border-bottom: 1px solid var(--rule); }
}

/* the rules that wrote rgba(245,242,234,…) literally */
html:not([data-theme="dark"]) .rail .rail-stack { background: var(--rule); }
html:not([data-theme="dark"]) .rail .rail-row:hover { background: #EFEADE; color: #2B2721; }
html:not([data-theme="dark"]) .rail .rail-group,
html:not([data-theme="dark"]) .rail .rail-special,
html:not([data-theme="dark"]) .rail .rail-quiet,
html:not([data-theme="dark"]) .rail .rail-portals a,
html:not([data-theme="dark"]) .rail .rail-close { border-color: #C6BBA7; }
html:not([data-theme="dark"]) .rail .rail-sub {
  border-color: #C6BBA7; border-left-color: #C9A227;
}
html:not([data-theme="dark"]) .rail .rail-fam { border-bottom-color: var(--rule); }
html:not([data-theme="dark"]) .rail .rail-items a {
  color: #2B2721; border-bottom-color: var(--rule);
}
html:not([data-theme="dark"]) .rail .rail-fambtn .fam-count,
html:not([data-theme="dark"]) .rail .rail-quiet,
html:not([data-theme="dark"]) .rail .rail-foot { color: #6C6558; }
html:not([data-theme="dark"]) .rail .rail-foot a { color: #4A443B; }

/* the wordmark block at the top of the desktop rail. .rail-head is
   hidden below 900px, so this only ever shows on a computer — which
   is exactly the part 0901c never had to think about. The tagline is
   var(--gold) #DFCD83, which on the white panel measures 1.56. */
html:not([data-theme="dark"]) .rail .rail-tag { color: #7E6C2B; }
html:not([data-theme="dark"]) .rail .rail-name { color: #2B2721; }
html:not([data-theme="dark"]) .rail .rail-close { color: #4A443B; }
html:not([data-theme="dark"]) .rail .rail-close:hover { border-color: #A0842B; color: #7E6C2B; }

/* the collapsed rail on a computer: the vertical label, the open
   button and the owl watermark all sat on the dark column */
html:not([data-theme="dark"]) .rail .rail-vert { color: #7E6C2B; }
html:not([data-theme="dark"]) .rail .rail-open { background: var(--gold); color: #2B2721; }
html:not([data-theme="dark"]) .rail .rail-open:hover { background: #EFE0A8; }
html:not([data-theme="dark"]) .rail .rail-mark { opacity: .38; }

/* aoe-theme-vars pins these bright gold for the dark column;
   on a light panel the deep gold is the readable one */
html:not([data-theme="dark"]) .rail .rail-label,
html:not([data-theme="dark"]) .rail .rail-row span,
html:not([data-theme="dark"]) .rail .rail-quiet span,
html:not([data-theme="dark"]) .rail .rail-fambtn .sign,
html:not([data-theme="dark"]) .rail .rail-group .sign,
html:not([data-theme="dark"]) .rail .rail-items .code,
html:not([data-theme="dark"]) .rail .rail-special span,
html:not([data-theme="dark"]) .rail .rail-portals a span { color: #75641F; }

/* the gold call to action keeps its dark text either way */
html:not([data-theme="dark"]) .rail .rail-cta,
html:not([data-theme="dark"]) .rail .rail-cta span { color: #2B2721; }
html:not([data-theme="dark"]) .rail .rail-cta:hover { background: #EFE0A8; }

/* hover states that borrowed --cream as a BACKGROUND */
html:not([data-theme="dark"]) .rail .rail-special:hover,
html:not([data-theme="dark"]) .rail .rail-quiet:hover,
html:not([data-theme="dark"]) .rail .rail-portals a:hover {
  border-color: #A0842B; color: #6A5B20;
}

/* the two switches in the rail are built for a dark ground:
   #aoe-lang fills itself rgba(43,39,33,.18) and colours its
   inactive half light. On the light panel that half measured 1.08
   — the same colour as the ground behind it. */
html:not([data-theme="dark"]) .rail .aoe-pill,
html:not([data-theme="dark"]) .rail #aoe-lang {
  background: #FFFFFF; border-color: #C6BBA7;
}
html:not([data-theme="dark"]) .rail .aoe-pill button,
html:not([data-theme="dark"]) .rail #aoe-lang button { color: #5F5950; }
html:not([data-theme="dark"]) .rail .aoe-pill button.on,
html:not([data-theme="dark"]) .rail #aoe-lang button.on {
  background: #DFCD83; color: #2B2721;
}


/* ============================================================
   13. WHAT THE RE-MEASURE TURNED UP
   ------------------------------------------------------------
   Every one of these was found by rendering the real pages and
   measuring, not by reading the stylesheets.
   ============================================================ */

/* --- (a) the homepage's gold button, in dark, was unreadable ---
   aoe-theme-vars round 5 pins `html[data-theme="dark"] .rail-stack a`
   to #F5F2EA so the dark rail's links read. "Free placement test"
   is an <a> inside .rail-stack AND the gold-filled .rail-cta, and
   the .rail-stack rule is one class more specific — so the gold
   button got light text on a gold fill. 1.42. Pre-existing, and
   the most prominent control on the homepage. */
html[data-theme="dark"] .rail .rail-stack a.rail-cta,
html[data-theme="dark"] .rail .rail-stack a.rail-cta span { color: #211E19; }

/* --- (b) the two switches, and the row markers, on the light
   rail. Both moved up into section 12 at 0902i, because the light
   rail is no longer phone-only and these have to follow it onto
   the desktop column. Nothing is left here. --------------------- */

/* --- (c) THE GOLD ON THE TAUPE BAND ---------------------------
   The open decision from 31p, carried in STATUS as item 15, now
   taken — because it turned out not to be only the eyebrow. On
   the light taupe band #6E675A, gold #EFE0A8 measures 4.24, and
   it is used for the eyebrow, every form label on the enquiry
   block, the breadcrumb and the row markers: a dozen pieces of
   small text on twelve public pages, all just under the 4.5 that
   small text needs.

   Lightened one step to #F5EBC4: 4.68 on the light band, 13.4 on
   the dark one. Same colour family, same brand, no other change.

   TO REVERT: delete this block. Nothing else depends on it. */
.hero, .hero-sub, .top, .bar, .band-taupe, .switchbar { --gold-deep: #F5EBC4; }
.hero .eyebrow, .hero-sub .eyebrow, .band-taupe .eyebrow, .top .eyebrow,
.hero label, .top label, .bar label, .band-taupe label,
.hero .crumb a, .top .crumb a, .band-taupe .crumb a { color: #F5EBC4; }

/* --- (d) three more things sitting on the taupe band ----------
   All three are gold #DFCD83 or a faded cream on #6E675A, and
   all three measured 3.5-3.9. */

/* the phone number and email in the enquiry block */
.band-taupe .contact-list a,
.band-taupe .contact-list p a { color: #F5EBC4; }

/* a ghost button on a band — .btn-outline was already handled in
   aoe-theme-vars, .btn-ghost was not */
.band-taupe .btn-ghost, .hero .btn-ghost, .top .btn-ghost, .bar .btn-ghost {
  color: #F5EBC4; border-color: rgba(245, 242, 234, .55);
}
.band-taupe .btn-ghost:hover, .hero .btn-ghost:hover,
.top .btn-ghost:hover, .bar .btn-ghost:hover {
  background: #F5EBC4; color: #2B2721; border-color: #F5EBC4;
}

/* the inactive half of a switch sitting on a band was cream at
   80% over a translucent dark fill over the band: 3.86 */
.masthead .aoe-pill.on-band button,
.masthead #aoe-lang button,
.top .aoe-pill.on-band button,
.top #aoe-lang button { color: #F5F2EA; }
.masthead .aoe-pill.on-band button.on,
.masthead #aoe-lang button.on,
.top .aoe-pill.on-band button.on,
.top #aoe-lang button.on { background: #DFCD83; color: #2B2721; }

/* --- (e) the "expired" chip in dark ---------------------------
   aoe-theme.css fills it with --wrong-line #C4553D and writes
   white on it: 4.45, just under what 10px text needs. The design
   system's own answer is that a FILLED meaning chip takes
   --on-meaning, which in dark is the ink — so the fill moves to
   the lighter --wrong and the text to the ink: 5.69. */
html[data-theme="dark"] .pill.bad {
  background: var(--wrong); border-color: var(--wrong); color: var(--on-meaning);
}


/* ============================================================
   14. THE HOMEPAGE WOULD NOT SCROLL
   ------------------------------------------------------------
   Reported: the homepage "crashed" and would not scroll down.
   It was not a crash. It was a scroll trap, and it is one line.

   style.css line 790:

     .rail-full, .shell.rail-shut .rail-collapsed {
       position: sticky; top: 0; max-height: 100vh;
       overflow-y: auto; overscroll-behavior: contain;
     }

   That is right on a computer: the rail is a column beside the
   page and it needs its own scrollbar so it never leaves a gap.

   The phone block below 900px resets `.rail` — position, height
   and overflow — but never resets `.rail-full`, the panel INSIDE
   it. So on a phone the directory panel stays a sticky, 100vh,
   own-scrolling box with `overscroll-behavior: contain`, and
   contain is exactly the instruction "do not pass a scroll on to
   the page behind me". On a phone that panel is most of the first
   screen, so almost every swipe landed inside it and the page
   never moved.

   Measured before this fix, at 390px: a wheel over the panel
   moved the page 0px against a document 4,182px tall.

   On a phone the panel is not a column and must not be a scroll
   container at all.
   ============================================================ */

@media (max-width: 900px) {
  .rail-full,
  .shell.rail-shut .rail-collapsed {
    position: static;
    max-height: none;
    overflow: visible;
    overscroll-behavior: auto;
  }
}


/* ============================================================
   15. MOTION
   ------------------------------------------------------------
   The rule this follows: motion tells you something happened, or
   where a thing came from. It never runs on its own to look
   busy, and it never runs where it would be a lie about speed.

   Three things it is deliberately NOT applied to:

     - the practice runner. A student is reading a question and
       answering it against the clock. Nothing moves that they
       did not move.
     - the game room DURING a game. It repaints from the server
       every two seconds; an entrance animation there would
       re-run every two seconds and flicker.
     - anything on the public pages that already has .reveal.
       index.html and its siblings carry their own observer with
       a 70ms stagger; running a second one over the top would
       double every entrance.

   Everything here is gated on `html.mo`, which aoe-app.js adds
   ONLY when the browser reports that reduced motion is not
   preferred. No class, no motion — and, just as important, an
   element with no `.mo-in` on it is fully visible. Nothing here
   can leave content hidden if a script fails, which is the usual
   way an entrance animation goes wrong.
   ============================================================ */

@keyframes mo-rise {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@keyframes mo-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes mo-slide {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes mo-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* --- a card arriving, once, as it comes into view ------------- */
html.mo .mo-in { animation: mo-rise .42s cubic-bezier(.22, .61, .36, 1) both; }

/* --- a fold opening: the content it reveals comes from under
       the summary, so the eye follows where it came from ------- */
html.mo .fold[open] > *:not(summary),
html.mo .smallprint[open] > *:not(summary) {
  animation: mo-slide .24s ease both;
}

/* --- a scoreboard tab: the board that appears fades in. It goes
       from display:none to shown, which restarts the animation
       by itself, so this only ever runs on a tap. -------------- */
html.mo .miniboard,
html.mo .boardbox table { animation: mo-fade .22s ease both; }

/* --- the message after a form comes back --------------------- */
html.mo .msg { animation: mo-slide .28s ease both; }

/* --- the gold rule under the active tab draws itself in ------- */
html.mo .aoe-tabs a.on::before {
  animation: mo-grow .32s cubic-bezier(.22, .61, .36, 1) both;
}

/* --- switching theme: a short crossfade instead of a hard cut.
       The class is on for 280ms and then removed, so this costs
       nothing at any other time. ------------------------------- */
html.mo-theme,
html.mo-theme *,
html.mo-theme *::before,
html.mo-theme *::after {
  transition: background-color .26s ease, border-color .26s ease,
              color .26s ease, fill .26s ease !important;
}

/* --- things that are links but look like blocks: they should
       answer a press the way a button does ---------------------- */
.way, .route, .rail-row, .rail-special, .rail-quiet, .rail-portals a,
.rail-cta, .erow .eopen, .renewrow, .setrow .btn {
  transition: transform .08s ease, opacity .08s ease;
}

@media (prefers-reduced-motion: reduce) {
  html.mo .mo-in,
  html.mo .fold[open] > *:not(summary),
  html.mo .smallprint[open] > *:not(summary),
  html.mo .miniboard, html.mo .boardbox table,
  html.mo .msg, html.mo .aoe-tabs a.on::before { animation: none; }
  html.mo-theme, html.mo-theme * { transition: none !important; }
  .way, .route, .rail-row, .rail-special, .rail-quiet,
  .rail-portals a, .rail-cta, .erow .eopen, .renewrow,
  .setrow .btn { transition: none; }
}

/* ============================================================
   16. QUESTION STEMS WITH A SCENARIO
   ------------------------------------------------------------
   Cambridge puts the situation above the command word, not
   inside it. The 0450 bank writes that as a line break in the
   stem. practice.php sets the text with textContent, so the
   break only shows if the CSS keeps it.
   pre-line keeps newlines and still collapses runs of spaces,
   which is exactly what is wanted here.
   ============================================================ */
.qtext, .qtext.big, .qbig { white-space: pre-line; }

/* ============================================================
   17. THE GLOSSARY DRILL (drill.php)
   ------------------------------------------------------------
   This page shipped with no styles at all — none of its classes
   existed in portal.css or here. It also fatal-errored on every
   open, so nobody ever saw that. Both are fixed together.

   It reuses the quiz chrome (.quizwrap, .quizbar, .qtitle) that
   portal.css already defines; everything below is the part that
   is unique to the drill.
   ============================================================ */
.drillhead { margin: 0 0 18px }
.drillmodes { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 6px }
.dirpick { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 4px 0 0 }
.dirpick .sub { margin: 0 4px 0 0 }

.pillink {
  display: inline-flex; align-items: center; min-height: 40px;
  padding: 8px 14px; border: 1px solid var(--rule); border-radius: 999px;
  font-size: 13px; text-decoration: none; color: var(--ink);
  background: var(--panel); transition: background .15s, border-color .15s;
}
.pillink:hover { border-color: var(--gold-deep) }
.pillink.on { background: var(--gold-deep); border-color: var(--gold-deep); color: #fff }

/* ---- the matching table ---- */
table.glossary { width: 100%; border-collapse: collapse; margin: 6px 0 0 }
table.glossary th, table.glossary td {
  border: 1px solid var(--rule); padding: 10px 12px;
  vertical-align: middle; font-size: 14px; line-height: 1.4;
}
table.glossary th { text-align: left; font-size: 12px; letter-spacing: .04em; text-transform: uppercase }
table.glossary td.given { background: var(--band, transparent) }

/* a cell waiting for a chip */
td.cell {
  min-height: 46px; min-width: 120px;
  outline-offset: -3px; cursor: pointer;
}
td.cell.ph { color: var(--ink-dim); font-style: italic }
td.cell:focus-visible { outline: 2px solid var(--gold-deep) }
td.cell.on { border-color: var(--gold-deep); box-shadow: inset 0 0 0 2px var(--gold-deep) }

/* the chips a student places */
.tray { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 0; min-height: 44px }
.chip {
  display: inline-flex; align-items: center; min-height: 40px;
  padding: 8px 12px; border: 1px solid var(--rule); border-radius: 8px;
  background: var(--panel); font-size: 13px; line-height: 1.3;
  cursor: grab; user-select: none; max-width: 100%;
}
.chip:active { cursor: grabbing }
.chip.on { border-color: var(--gold-deep); box-shadow: 0 0 0 2px var(--gold-deep) inset }
.chip.ghost { opacity: .35 }
.chip[disabled] { cursor: default; opacity: .5 }

/* ---- type the term ---- */
.clozecard { padding: 4px 0 0 }
.clozedef { font-size: 17px; line-height: 1.5; margin: 0 0 14px }
.clozeask { font-size: 13px; color: var(--ink-dim); margin: 0 0 10px }
.blankrow { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start }

/* One word per box, with a ruler under it showing how many letters
   are still to come. A placeholder would vanish on the first
   keystroke, which is the whole reason this exists. */
.blankwrap { display: inline-flex; flex-direction: column; gap: 5px }
input.blank {
  min-height: 46px; padding: 9px 12px; font-size: 17px;
  font-family: "IBM Plex Mono", ui-monospace, monospace; letter-spacing: .06em;
  border: 1px solid var(--rule); border-radius: 8px;
  background: var(--panel); color: var(--ink); width: 100%; min-width: 8ch;
}
input.blank:focus-visible { outline: 2px solid var(--gold-deep); outline-offset: 1px }

.ruler { display: flex; gap: 4px; padding: 0 2px }
.ruler .sl {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 20px; font-style: normal;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 13px; line-height: 1; color: var(--ink-dim);
  border-bottom: 2px solid var(--rule);
}
.ruler .sl.on { color: var(--ink); border-bottom-color: var(--gold-deep) }
.ruler.full .sl { border-bottom-color: var(--right, var(--gold-deep)) }
.ruler.over .sl { border-bottom-color: var(--wrong, #b3261e) }

.drillact { margin: 18px 0 0; display: flex; flex-wrap: wrap; gap: 10px; align-items: center }
.qmsg { margin: 12px 0 0; font-size: 14px; line-height: 1.5 }
.dim2 { color: var(--ink-dim) }

@keyframes drill-shake {
  0%,100% { transform: translateX(0) } 25% { transform: translateX(-5px) }
  75% { transform: translateX(5px) }
}
.shake { animation: drill-shake .28s ease-in-out 2 }
@media (prefers-reduced-motion: reduce) { .shake { animation: none } }

@media (max-width: 560px) {
  .blankrow { gap: 10px }
  input.blank { font-size: 16px }        /* 16px stops iOS zooming on focus */
  table.glossary th, table.glossary td { padding: 8px 9px; font-size: 13px }
}

/* ============================================================
   18. ANSWER CONFIRMATION (practice.php)
   ------------------------------------------------------------
   The three-second countdown is gone. A tap selects; the Check
   button commits. Same protection against a mis-tap, without
   making the student who tapped correctly wait for nothing.
   ============================================================ */
.qgo { margin: 14px 0 6px }
.qgo .btn[disabled] { opacity: .45; cursor: default }
.qbtn.pick { border-color: var(--gold-deep); box-shadow: 0 0 0 2px var(--gold-deep) inset }
.qbtn.pick .k { background: var(--gold-deep); color: #fff }

/* ============================================================
   §20  Question diagrams  (build 0902i)
   A figure in a practice question is a scientific drawing, so it
   does NOT follow the light/dark theme. It sits in its own box
   with a white ground and a fixed border, and looks the same on
   every screen and the same as the printed paper.
   The <figure> starts hidden and JS reveals it only once the file
   has actually loaded, so a bank can ship before its artwork does.
   ============================================================ */
.qfig{
  margin:0 0 20px;
  padding:16px 14px;
  background:#fff;                 /* fixed: never a theme variable */
  border:1px solid #C9C2B2;        /* fixed, so it reads on both grounds */
  border-radius:2px;
  overflow-x:auto;
  text-align:center;
  color:#000;                      /* SVGs use currentColor for line work */
}
.qfig img{
  display:block;
  margin:0 auto;
  max-width:100%;
  height:auto;
  max-height:46vh;
}
@media (max-width:520px){
  .qfig{padding:12px 10px;margin-bottom:16px}
  .qfig img{max-height:38vh}
}
/* inside the review card the figure is smaller — the answer matters more there */
.qcard .qfig{margin-bottom:14px;padding:12px}
.qcard .qfig img{max-height:30vh}

/* a diagram that will not load must say so — the question needs it (0902i) */
.qfig-miss{
  margin:0; padding:10px 12px;
  font-size:14px; line-height:1.5;
  color:#8A4E44; background:#FBEEEC;
  border:1px solid #E3C9C4; border-radius:2px;
}

/* 0905k — a sign-out is a POST (it changes state), so it is a button
   dressed as the link it used to be. */
.aoe-logout{display:inline;margin:0}
.linklike{background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;cursor:pointer;text-decoration:none}
.linklike:hover,.linklike:focus-visible{text-decoration:underline}
.bar .linklike,.masthead nav .linklike{color:var(--gold,#DFCD83)}

/* 0905m — the toast: one line, bottom centre, gone in a moment */
.aoe-toast{position:fixed;left:50%;bottom:calc(72px + env(safe-area-inset-bottom,0px));transform:translate(-50%,8px);
  background:var(--ink,#2B2721);color:var(--cream,#F5F2EA);padding:10px 16px;font-size:14px;letter-spacing:.02em;
  border:1px solid rgba(255,255,255,.18);opacity:0;pointer-events:none;z-index:200;transition:opacity .18s ease,transform .18s ease}
.aoe-toast.on{opacity:1;transform:translate(-50%,0)}
@media (min-width:901px){.aoe-toast{bottom:28px}}

/* ============================================================
   18. MOTION, PART TWO (0905m)
   ------------------------------------------------------------
   Progress bars grow from the left and the 30-day chart rises
   from its baseline the first time their card comes into view;
   folds open and close with height instead of a jump (the JS in
   aoe-app.js drives that, and it does nothing without html.mo);
   block-like links lift a touch on hover. All of it is gated on
   html.mo, which is only set when reduced motion is NOT asked for.
   ============================================================ */
@keyframes mo-growx { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes mo-riseY { from { transform: scaleY(0); } to { transform: scaleY(1); } }

html.mo .mo-in .barline span,
html.mo .mo-in .cycbar span,
html.mo .mo-in .wk-bar span,
html.mo .mo-in .avbar span {
  transform-origin: left center;
  animation: mo-growx .7s cubic-bezier(.22, .61, .36, 1) both;
}
html.mo .mo-in .dashchart rect.on {
  transform-box: fill-box; transform-origin: center bottom;
  animation: mo-riseY .55s cubic-bezier(.22, .61, .36, 1) both;
}
html.mo .mo-in .dashchart rect:nth-child(n+2)  { animation-delay: .02s }
html.mo .mo-in .dashchart rect:nth-child(n+6)  { animation-delay: .06s }
html.mo .mo-in .dashchart rect:nth-child(n+11) { animation-delay: .11s }
html.mo .mo-in .dashchart rect:nth-child(n+16) { animation-delay: .16s }
html.mo .mo-in .dashchart rect:nth-child(n+21) { animation-delay: .21s }
html.mo .mo-in .dashchart rect:nth-child(n+26) { animation-delay: .26s }

/* a fold mid-animation: the box clips its content while the height moves */
html.mo details.mo-folding { overflow: hidden; }
html.mo details.mo-folding > summary { position: relative; z-index: 1; }

/* block links lift a touch; cards with an action inside answer hover */
html.mo .way, html.mo .sub, html.mo .route, html.mo .td, html.mo .sec.open {
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
html.mo .way:hover, html.mo .sub:hover, html.mo .route:hover, html.mo .td:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 24px -14px rgba(43, 39, 33, .35);
}
html.mo .btn, html.mo .pillink, html.mo .chips a, html.mo .tabs a { transition: background-color .15s ease, color .15s ease, border-color .15s ease, transform .08s ease; }
html.mo .btn:focus-visible { outline: 2px solid var(--gold-deep, #A08C42); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  html.mo .mo-in .barline span, html.mo .mo-in .cycbar span, html.mo .mo-in .wk-bar span, html.mo .mo-in .avbar span,
  html.mo .mo-in .dashchart rect.on { animation: none; }
  html.mo .way:hover, html.mo .sub:hover, html.mo .route:hover, html.mo .td:hover { transform: none; box-shadow: none; }
}


/* ============================================================
   21. FORMS THAT HELP                            (build 0905n)
   ------------------------------------------------------------
   Four things, all of them phone-first:

     (a) the primary action on a phone is the full width of the
         card, so the thumb cannot miss it. Secondary actions
         (ghost, danger, tiny) stay their own size, and anything
         inside a table keeps its size too — an admin row of six
         small buttons must not become six stacked bars.
     (b) a button that has been pressed says what it is doing.
         .is-busy already dimmed it and added a "…"; when the
         WORDS have changed the dots would be a second ellipsis,
         so they come off.
     (c) a 2px line across the top of the screen while a page is
         on its way. It is indeterminate on purpose: this site
         has no way of knowing how far a POST has got, and a bar
         that pretends to know is a lie that stalls at 80%.
     (d) the answer beside the field — an email that is missing
         its domain, a password too short, two that do not match.

   Everything here is a token; no raw hex except the one warning
   colour, which is the same #B0442F portal.css already uses for
   .btn.danger and .msg.err.
   ============================================================ */

/* ---- (a) the thumb zone ---- */
@media (max-width: 480px) {
  form .btn:not(.tiny):not(.ghost):not(.danger),
  .lockacts .btn:not(.tiny),
  .secacts .btn:not(.tiny),
  .pstrip-acts .btn:not(.tiny) {
    display: flex; width: 100%; text-align: center;
  }
  /* a table of actions is not a stack of bars */
  table .btn, .acts .btn, .mini .btn, .data .btn, .tablescroll .btn {
    display: inline-flex; width: auto;
  }
  /* two actions that belong together, side by side */
  .btnrow { display: grid; grid-template-columns: 1fr; gap: 10px; }
  .btnrow.two { grid-template-columns: 1fr 1fr; }
  .btnrow .btn { width: 100%; display: flex; }
}

/* ---- (b) the button says what it is doing ---- */
.is-busy[data-busy-on] { opacity: .82; }
.is-busy[data-busy-on]::after { content: none; }

/* ---- (c) the line that says a page is coming ---- */
.aoe-line {
  position: fixed; left: 0; right: 0; top: 0; height: 2px;
  z-index: 300; overflow: hidden; pointer-events: none;
  opacity: 0; transition: opacity .15s ease;
}
.aoe-line.on { opacity: 1; }
.aoe-line::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: -38%; width: 38%;
  background: var(--gold, #DFCD83);
  animation: aoe-sweep 1.05s linear infinite;
}
@keyframes aoe-sweep { from { left: -38%; } to { left: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .aoe-line::before { animation: none; left: 0; width: 100%; opacity: .55; }
}

/* ---- (d) the answer beside the field ---- */
.fielderr {
  display: block; margin-top: 6px;
  font-size: 13px; line-height: 1.45; color: #B0442F;
}
html[data-theme="dark"] .fielderr { color: #E29A85; }
input[aria-invalid="true"], textarea[aria-invalid="true"] { border-color: #B0442F; }

/* ---- Show / Hide on a password ---- */
.pwwrap { position: relative; display: block; }
.pwwrap input { padding-right: 68px; }
.pweye {
  position: absolute; top: 0; right: 0; height: 100%;
  padding: 0 12px; border: 0; background: transparent; cursor: pointer;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--taupe-dark, #7C7466);
}
.pweye:hover { color: var(--gold-deep, #A08C42); }
.pweye:focus-visible { outline: 2px solid var(--gold-deep, #A08C42); outline-offset: -2px; }

/* a code box is read four characters at a time, so it wants the
   space between them that portal.css already gives .codebox —
   this only extends it to the boxes that are not in a .redeem row */
input[data-code], .codebox {
  font-family: "IBM Plex Mono", "Consolas", monospace;
  text-transform: uppercase; letter-spacing: .12em;
}

/* ============================================================
   21. KEYBOARD, SCREEN READERS AND THE 11px FLOOR    (0906a)
   ------------------------------------------------------------
   UI-IDEAS #6 and #7.
   ============================================================ */

/* --- 0. this file's own labels, which were 10 to 10.5px ------ */
.emeta, .setfoot, .aoe-tip, .fsec > .fsechead,
.smallprint > summary, .aoe-fresh button, .eopen { font-size: 11px; }
@media (max-width: 560px) {
  table.mini.keyval td:first-child { font-size: 11px; }
}
/* the two switches in the phone masthead were 10px */
@media (max-width: 760px) {
  body.has-tabs .masthead .aoe-pill button,
  body.has-tabs .masthead #aoe-lang button {
    font-size: 11px; letter-spacing: .06em; padding: 8px 10px; line-height: 1;
  }
}

/* --- 1. the skip link ---------------------------------------
   Injected by the shared script on any page that has not
   printed one itself (aoe-ui.php prints it server-side, so a
   page with JS off still has it). Off-screen until it is
   tabbed to, and then it is the first thing on the page. */
.aoe-skip {
  position: fixed; left: -9999px; top: 0; z-index: 999;
  background: var(--accent, #A08C42); color: #F5F2EA;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
  padding: 12px 18px; text-decoration: none; border-radius: 0 0 2px 0;
}
.aoe-skip:focus, .aoe-skip:focus-visible { left: 0; top: 0; }
/* the target is given tabindex=-1 so the focus really lands there;
   it must not then draw a ring of its own */
[data-skip-target]:focus { outline: none; }

/* --- 2. ONE focus ring, and only for a keyboard --------------
   :focus-visible, never :focus. The @supports guard means a
   browser too old to understand :focus-visible keeps the ring
   it has today rather than losing focus altogether — the safe
   direction to fail in. */
:focus-visible {
  outline: 2px solid var(--accent, #A08C42);
  outline-offset: 2px;
}
@supports selector(:focus-visible) {
  :focus:not(:focus-visible) { outline: none; }
  /* the 3px gold halo on a form field was on :focus, so it fired
     on every mouse click into a box */
  input:focus:not(:focus-visible),
  select:focus:not(:focus-visible),
  textarea:focus:not(:focus-visible) { box-shadow: none; }
  .team .tname:focus:not(:focus-visible) { outline: none; }
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent, #A08C42); outline-offset: 1px;
}
/* a control on a dark band needs the light half of the palette */
.top :focus-visible, .hero :focus-visible, .bar :focus-visible,
.band-taupe :focus-visible, .rail :focus-visible,
.quiz-body :focus-visible, .board-body :focus-visible,
.pk-body :focus-visible, .aoe-tabs :focus-visible {
  outline-color: #EFE0A8;
}

/* --- 3. nothing under 11px -----------------------------------
   Every mono label on the site, floored at 11px; every sentence
   at 13px. The sizes below were 9px to 10.5px, which is under
   what a phone can render as a word rather than a texture. */
.tag, .passage .passage-tag, .qopt .mark, .plat-tag, .route-tag,
.setcard.planned .pill, .donetag, .qscore .cmb, .endstats span,
table.data th, .pill, .sec .state, .substat span, .boardtabs button,
.dashaxis, .minifoot, .es-tmeta, .es-mk-name, .es-sev, .es-crit,
.es-before span, .es-after span, .es-tlabel, .nbadge,
.rail-vert, .rail-label, .rail-fambtn .fam-count, .way-kicker,
.aoe-pill.mono button, .textsize, .rnum, .rfig .lbl, .verdictpill,
.emeta, table.mini.keyval td:first-child, .setfoot, .aoe-tip,
.fsec > .fsechead, .smallprint > summary, .aoe-fresh button,
.st-tag, .st-set > .lb, .st-seg button small, .st-head .meta,
.st-kick, .st-table th, .st-more-lb {
  font-size: 11px;
}
/* sentences, not labels */
.lang-note, .rail-foot, .pk-note, .es-quote, .es-mk-top,
.rfig .delta, .rfig .vs, .btn.tiny {
  font-size: 13px;
}

/* --- 4. 中文 reads differently -------------------------------
   Spaced Chinese reads as broken, and Chinese body copy needs
   more room between lines than English does. i18n.js puts .zh
   on <html> in the zh branch of apply(); these are the two
   lines that class exists for. */
html.zh body { line-height: 1.8; }
html.zh .eyebrow,
html.zh .tag, html.zh .passage .passage-tag, html.zh .qopt .mark,
html.zh .plat-tag, html.zh .route-tag, html.zh .donetag,
html.zh .qscore .cmb, html.zh .endstats span, html.zh table.data th,
html.zh .pill, html.zh .sec .state, html.zh .substat span,
html.zh .boardtabs button, html.zh .dashaxis, html.zh .minifoot,
html.zh .es-tmeta, html.zh .es-mk-name, html.zh .es-sev,
html.zh .es-crit, html.zh .es-tlabel, html.zh .rail-label,
html.zh .rail-fambtn .fam-count, html.zh .way-kicker,
html.zh .aoe-pill.mono button, html.zh .rnum, html.zh .rfig .lbl,
html.zh .verdictpill, html.zh .emeta, html.zh .setfoot,
html.zh .fsec > .fsechead, html.zh .smallprint > summary,
html.zh .crumb, html.zh .crumbline, html.zh .catmeta,
html.zh .qcount, html.zh .cycleline, html.zh .aoe-tabs span,
html.zh label, html.zh .card label {
  letter-spacing: .04em;
}

/* ============================================================
   19. CLASSES FIRST, THE CLASS PAGE, AND THE PORTAL SWITCH
   ------------------------------------------------------------
   Batch 6 (UI-IDEAS 27, 28, 29). Everything here is scoped to
   classes that exist only on the teacher portal and the class
   page, except .aoe-sides, which is the header switch on both
   portals.
   ============================================================ */

/* ---- 29. Learners <-> Teacher, as a pill ----
   The pill is a <form> of two submit buttons, so it inherits every
   .aoe-pill rule already written for the two switches, including the
   on-band contrast fixes in section 13. */
.masthead .aoe-sides { margin: 0; }
.masthead .aoe-sides button { cursor: pointer; }

/* Three pills do not fit a 390px header: EN|中文 and LIGHT|DARK
   already use it all. On a phone the switch stands down — the
   bottom bar carries Learn on the teacher side — and the header
   stays the single row job J left it. */
@media (max-width: 760px) {
  body.has-tabs .masthead .aoe-pill.aoe-sides { display: none; }
}

/* ---- 28. the class band ----
   Four figures in one mono line, then the two buttons a teacher
   presses during a lesson. Everything else folds. */
.clsfigs {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 18px;
  margin: 0 0 16px;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 12px; letter-spacing: .04em; color: var(--taupe-dark);
}
.clsfigs > span { display: inline-flex; align-items: baseline; gap: 5px; }
.clsfigs > span + span::before { content: "·"; margin-right: 13px; opacity: .6; }
.clsfigs b {
  font-family: "Marcellus", Georgia, serif; font-weight: 400;
  font-size: 19px; letter-spacing: 0; color: var(--text, #2B2721);
}
.clsfigs .pill { letter-spacing: .1em; }

.clsmain { display: flex; gap: 10px; margin: 0; }
.clsmain .btn { margin: 0; }
.clsmore { margin-top: 14px; }
.clsmore > summary { font-size: 13px; }
.clsacts { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 0; }
.clsacts .btn { margin: 0; }

/* The primary action belongs in the thumb zone and full width — the
   rule UI-IDEAS 3 sets for every phone. */
@media (max-width: 560px) {
  .clsmain { flex-direction: column; }
  .clsmain .btn { width: 100%; text-align: center; }
  .clsfigs { gap: 4px 14px; }
  .clsfigs > span + span::before { margin-right: 9px; }
}

/* ---- 28. students: a table on a laptop, rows on a phone ----
   The table is six columns wide and was cut off at 390px with a
   sideways scroll. Below 760px it stands down and the list takes
   over: one row per student, the whole row a tap target. */
.stulist { display: none; list-style: none; margin: 0; padding: 0; }
.stulist li {
  display: flex; align-items: center; gap: 10px;
  border-top: 1px solid var(--rule);
}
.stulist li:first-child { border-top: 0; }
.stulist .srow {
  flex: 1 1 auto; min-width: 0; display: block;
  padding: 13px 0; text-decoration: none; color: inherit;
}
.stulist .srow:hover .sname { text-decoration: underline; }
.stulist .sname {
  display: block; font-family: "Marcellus", Georgia, serif; font-size: 17px;
}
.stulist .smeta {
  display: block; margin-top: 3px;
  font-family: "IBM Plex Mono", "Consolas", monospace;
  font-size: 11px; letter-spacing: .04em; line-height: 1.7;
  color: var(--taupe-dark);
}
.stulist .smeta b { font-weight: 500; color: var(--text, #2B2721); }
.stulist .sdel { flex: 0 0 auto; margin: 0; }

@media (max-width: 760px) {
  .stutable { display: none; }
  .stulist { display: block; }
}

/* ---- 27. teaching tools, one 2-up row ---- */
.toolrow { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 0; }
.toolrow .btn { margin: 0; text-align: center; }
@media (max-width: 480px) { .toolrow { grid-template-columns: 1fr; } }

/* end of section 19 */

/* ============================================================
   In-page confirms and the Undo toast   (batch 7, UI-IDEAS 4)
   ============================================================ */

dialog.aoe-cf {
  border: 1px solid var(--rule-c, #E2DBCB);
  background: var(--raised, #fff);
  color: var(--text, #2B2721);
  padding: 0;
  width: min(440px, calc(100vw - 32px));
  max-height: calc(100vh - 32px);
  box-shadow: 0 18px 50px rgba(20, 18, 15, .22);
}
dialog.aoe-cf::backdrop { background: rgba(20, 18, 15, .55); }

.aoe-cf-in { padding: 22px 22px 18px; }

.aoe-cf-q {
  font-family: Marcellus, Georgia, serif;
  font-weight: 400;
  font-size: 21px;
  line-height: 1.3;
  margin: 0;
}
/* the row that names WHICH one — a class, a code, a person. Never
   translated (it carries data), so it is set in mono to read as data. */
.aoe-cf-d {
  font-family: "IBM Plex Mono", ui-monospace, Consolas, monospace;
  font-size: 13px;
  line-height: 1.5;
  color: var(--accent, #A08C42);
  margin: 8px 0 0;
  word-break: break-word;
}
.aoe-cf-b {
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--dim, #7C7466);
  margin: 10px 0 0;
}
.aoe-cf-b:empty { display: none; }
.aoe-cf-d[hidden] { display: none; }

.aoe-cf-acts { display: flex; gap: 8px; margin: 20px 0 0; justify-content: flex-end; }
.aoe-cf-acts .btn { margin: 0; }

/* red only where the press is one-way */
dialog.aoe-cf.is-danger { border-left: 3px solid var(--bad, #B0442F); }
dialog.aoe-cf.is-danger .aoe-cf-yes {
  background: var(--bad, #B0442F);
  border-color: var(--bad, #B0442F);
  color: #fff;
}
/* money is not danger — nothing is destroyed — but it is one-way */
dialog.aoe-cf.is-money { border-left: 3px solid var(--accent, #A08C42); }

dialog.aoe-cf .btn:focus-visible {
  outline: 2px solid var(--accent, #A08C42);
  outline-offset: 2px;
}

/* Phones: full-width buttons, and the SAFE one is the lower of the
   two. Reversing the desktop order is deliberate — the bottom of a
   phone screen is where the thumb already is, and that is the last
   place a "Delete the class" button should be. */
@media (max-width: 480px) {
  dialog.aoe-cf { width: calc(100vw - 24px); }
  .aoe-cf-in { padding: 20px 18px 16px; }
  .aoe-cf-acts { flex-direction: column; }
  .aoe-cf-acts .btn { width: 100%; text-align: center; }
}

html.mo dialog.aoe-cf[open] { animation: aoeCfIn .16s ease-out both; }
@keyframes aoeCfIn {
  from { opacity: 0; transform: translateY(6px) scale(.99); }
  to   { opacity: 1; transform: none; }
}

/* ---------- the Undo toast ---------- */
.aoe-undo {
  position: fixed;
  left: 50%;
  bottom: calc(88px + env(safe-area-inset-bottom, 0px));  /* clear of the phone bar */
  transform: translateX(-50%) translateY(8px);
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 14px;
  /* shrink-to-fit picks a needlessly narrow box and breaks two words
     onto two lines; max-content sizes it to the sentence and the
     max-width still keeps it inside a 390px screen */
  width: max-content;
  max-width: calc(100vw - 28px);
  padding: 11px 12px 11px 16px;
  background: var(--text, #2B2721);
  color: #F5F2EA;
  border: 1px solid rgba(245, 242, 234, .16);
  box-shadow: 0 10px 30px rgba(20, 18, 15, .3);
  opacity: 0;
}
/* display:flex above beats the UA's [hidden]{display:none}, so the
   toast has to be told to stay hidden when it is hidden. Without this
   line an Undo that has been taken back leaves the bar on screen. */
.aoe-undo[hidden] { display: none; }
.aoe-undo.on { opacity: 1; transform: translateX(-50%) translateY(0); }
html.mo .aoe-undo { transition: opacity .18s ease, transform .18s ease; }

.aoe-undo-t { font-size: 14.5px; line-height: 1.4; }
.aoe-undo-b {
  font-family: "IBM Plex Mono", ui-monospace, Consolas, monospace;
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  background: none;
  border: 1px solid rgba(245, 242, 234, .4);
  color: #DFCD83;
  padding: 6px 12px;
  cursor: pointer;
  flex: none;
}
.aoe-undo-b:hover { border-color: #DFCD83; }
.aoe-undo-b:focus-visible { outline: 2px solid #DFCD83; outline-offset: 2px; }

@media (min-width: 761px) {
  .aoe-undo { bottom: calc(24px + env(safe-area-inset-bottom, 0px)); }
}
@media (prefers-reduced-motion: reduce) {
  dialog.aoe-cf[open] { animation: none; }
  .aoe-undo { transition: none; }
}
