/* Reusable schedule viewer modal for the SMTP programme page (Foundation / Advanced / School
   Supervisor). Loaded on both the EN and TC proj-smtp pages; relies on the --primary/--ink/etc.
   custom properties each page already defines in its own inline <style> block — custom
   properties resolve at computed-value time, so the load order relative to this external
   stylesheet doesn't matter. Paired with assets/schedule-modal.js. */

/* Scroll lock: html + body both get overflow:hidden, plus overscroll-behavior:none so a wheel/
   trackpad gesture that runs past the modal's own scroll limits never chains into the page
   underneath. Deliberately NOT paired with the classic "pin body with position:fixed and a
   negative top offset" trick — that would pull the site header (position:sticky) out of its
   scrolling context and off-screen, since there'd be nothing left for it to stick to. Chrome can
   still let the root document scroll with only overflow:hidden, so schedule-modal.js additionally
   intercepts wheel/touchmove at the document level (preventBackgroundScroll()) as the remaining
   safeguard — without ever touching the document's scroll position.

   Separately: switching html/body's overflow at all is itself a change to the root scrolling
   container, and in Chrome that can make an already-stuck position:sticky header drop back to its
   static (unstuck, off-screen) position for as long as the modal is open. schedule-modal.js's
   pinSiteHeader()/unpinSiteHeader() sidestep this by temporarily switching the header to
   position:fixed (see .schedule-modal-header-pinned below) and holding its layout space open with
   a placeholder, so nothing beneath it shifts and it never has a "stuck" state to lose. */
html.schedule-modal-open,
body.schedule-modal-open{
  overflow:hidden;
  overscroll-behavior:none;
}

/* Temporary stand-in for header.site-header's own layout space while it's pinned below, so the
   page content that follows it doesn't jump upward to fill the gap. Purely a spacer — no visual
   styling of its own. */
.schedule-modal-header-placeholder{
  width:100%;
}

/* z-index:40 matches the header's own normal z-index (see every page's own inline <style>) — it
   must stay identical while pinned so its stacking relative to the backdrop (z-index:1000, see
   below) doesn't change; only `position` moves from sticky to fixed. The `right` value reads the
   scrollbar-width custom property lockPageScroll() sets on :root, so the pinned header stays
   exactly as wide as the scrollbar-compensated body and never overhangs or gaps against it. */
header.site-header.schedule-modal-header-pinned{
  position:fixed !important;
  top:0;
  left:0;
  right:var(--schedule-modal-scrollbar-width, 0px);
  width:auto;
  z-index:40;
}

/* z-index:1000 is deliberately ABOVE header.site-header's z-index:40 (see every page's own inline
   <style>): the backdrop must dim the site header along with the rest of the page — nav, logo,
   language switcher, page content, footer, the whole viewport — not just everything below it.
   The header itself is never hidden, moved, or made non-interactive directly; it simply sits
   behind this translucent layer like everything else, so it stays visible but inactive while the
   modal is open, and becomes fully clear again automatically as this backdrop fades out on close. */
.schedule-modal-overlay{
  position:fixed; inset:0; z-index:1000;
  display:flex; align-items:center; justify-content:center;
  padding:32px;
  background:rgba(15,18,38,0.55);
  opacity:0;
  transition:opacity 200ms cubic-bezier(0.22,1,0.36,1);
}
.schedule-modal-overlay[hidden]{ display:none; }

/* Entrance: fades in and settles from a slight downward/scaled-down offset. Exit (.is-leaving)
   swaps in faster, slightly different end values, per the JS-driven open/close sequence in
   schedule-modal.js — the overlay is only actually hidden once this transition (or its fallback
   timeout) completes, so the panel never just vanishes mid-animation. */
.schedule-modal{
  width:min(1080px, calc(100vw - 64px));
  max-height:82dvh;
  display:flex; flex-direction:column;
  background:#fff; border-radius:16px;
  box-shadow:0 30px 80px rgba(10,14,40,0.35);
  overflow:hidden;
  opacity:0;
  transform:translateY(10px) scale(0.985);
  transition:opacity 240ms cubic-bezier(0.22,1,0.36,1), transform 240ms cubic-bezier(0.22,1,0.36,1);
}

.schedule-modal-overlay.is-visible{ opacity:1; }
.schedule-modal-overlay.is-visible .schedule-modal{ opacity:1; transform:translateY(0) scale(1); }

.schedule-modal-overlay.is-leaving{ transition-duration:180ms; opacity:0; }
.schedule-modal-overlay.is-leaving .schedule-modal{
  transition-duration:180ms;
  opacity:0;
  transform:translateY(8px) scale(0.99);
}

.schedule-modal__header{
  flex:0 0 auto;
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding:18px 28px;
  border-bottom:1px solid var(--border);
  background:#fff;
}
/* Two-part title: the dynamic programme name (schedule-modal__programme-name) carries the
   principal emphasis, the fixed "Schedule"/"日程表" label (schedule-modal__title-type) sits beside
   it as a secondary — but still bold, not caption-like — element. Both are set in em relative to
   this font-size, so they scale together at the mobile breakpoint below rather than needing their
   own overrides. flex-wrap lets the type label drop to its own line on narrow screens instead of
   ever shrinking below its own readable size. Gap is deliberately smaller for Chinese (0.3em) than
   English (0.45em) below — 基礎課程/日程表 need less separation than two English words to still read
   as one title with two levels of emphasis, rather than as touching or as unrelated headings. */
.schedule-modal__title{
  margin:0; min-width:0; font-family:var(--display); font-size:1.3rem;
  line-height:1.35;
  display:flex; align-items:baseline; flex-wrap:wrap; gap:0.45em;
}
html[lang^="en"] .schedule-modal__title{ gap:0.28em; }
html[lang^="zh"] .schedule-modal__title{ gap:0.3em; }
.schedule-modal__programme-name{
  font-size:1em; font-weight:700; color:var(--primary-dark);
}
/* One shared colour for both languages — a neutral slate-grey, deliberately not var(--ink-muted)
   (reads too pale next to the deep indigo programme name) and deliberately not var(--primary)
   (the brand blue belongs to the programme name alone). Font size still differs per language
   below for optical balance, but colour is set once here so "Schedule" and "日程表" are always the
   exact same value. */
.schedule-modal__title-type{
  font-size:0.76em; font-weight:700; color:#646977;
}
/* English "Schedule" reads slightly larger than the Chinese "日程表" needs to at the same weight.
   The 1px top offset is a small optical correction: at this size/weight "Schedule"'s cap-height
   sits very slightly high of the programme name's baseline-aligned box otherwise. */
html[lang^="en"] .schedule-modal__title-type{
  font-size:0.82em;
  position:relative;
  top:1px;
}
/* Groups the past-lessons toggle with the Close button so they read as one cluster of header
   controls, right-aligned opposite the title (.schedule-modal__header keeps its own
   justify-content:space-between for that). At mobile widths this wrapper is unwrapped via
   display:contents (see the media query below) so its two children can be placed independently
   in a two-row grid instead — the toggle drops to its own compact row, Close stays with the title. */
.schedule-modal__header-actions{
  flex:0 0 auto; display:flex; align-items:center; gap:20px;
}

/* Compact text action, not a pill/button/switch — deliberately plain until hover/focus, so it
   stays visually secondary to both the title and the Close button beside it. */
.schedule-modal__past-toggle{
  appearance:none; flex:0 0 auto;
  border:0; border-radius:4px; padding:6px 2px;
  background:transparent; color:var(--ink-muted);
  font:inherit; font-family:var(--sans); font-size:0.84rem; font-weight:600; line-height:1.2;
  white-space:nowrap; cursor:pointer;
  transition:color .15s ease;
}
.schedule-modal__past-toggle[hidden]{ display:none; }
.schedule-modal__past-toggle:hover,
.schedule-modal__past-toggle:focus-visible{
  color:var(--primary);
  text-decoration:underline; text-underline-offset:3px;
}

.schedule-modal__close{
  flex:0 0 auto; display:inline-flex; align-items:center; gap:6px;
  padding:8px 18px; border-radius:999px; border:1px solid var(--border);
  background:#fff; color:var(--ink-muted); font-size:0.86rem; font-weight:600;
  font-family:var(--sans); cursor:pointer;
  transition:background-color .15s ease, color .15s ease, border-color .15s ease;
}
.schedule-modal__close:hover{ background:var(--primary-soft); color:var(--primary); border-color:var(--primary); }
.schedule-modal__close:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; background:var(--primary-soft); color:var(--primary); }

/* Deliberately NOT the amber/orange treatment used by the site's enrolment CTAs (var(--orange)) —
   this is a secondary document action, not an invitation to enrol. A thin primary-coloured
   outline distinguishes it from the neutral-grey Close button beside it without competing for
   attention the way a filled/branded button would. */
/* Starts invisible/offset — 2026-08: the button is created hidden and only revealed (via
   revealDownloadPdfButton() in schedule-modal.js, which adds .is-ready two animation frames after
   unhiding it) once the schedule behind it has actually loaded, validated, normalised and
   rendered. .is-ready and .is-generating are deliberately separate, non-overlapping meanings —
   "the button has appeared" vs. "a download is in progress" — so the brief one-off reveal below
   never fires again on every retry/toggle, and the generation pulse never plays while the button
   is merely appearing. */
.schedule-modal__download-pdf{
  flex:0 0 auto; display:inline-flex; align-items:center; gap:6px;
  padding:8px 16px; border-radius:999px; border:1px solid var(--primary);
  background:#fff; color:var(--primary); font-size:0.82rem; font-weight:600;
  font-family:var(--sans); cursor:pointer; white-space:nowrap;
  opacity:0; transform:translateY(3px) scale(0.985);
  transition:opacity 180ms ease-out, transform 180ms cubic-bezier(0.22,1,0.36,1),
    background-color .15s ease;
}
.schedule-modal__download-pdf.is-ready{ opacity:1; transform:translateY(0) scale(1); }
.schedule-modal__download-pdf[hidden]{ display:none; }
.schedule-modal__download-pdf:hover{ background:var(--primary-soft); }
.schedule-modal__download-pdf:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; background:var(--primary-soft); }
.schedule-modal__download-pdf:disabled{ cursor:default; opacity:0.7; }
.schedule-modal__download-pdf:disabled:hover{ background:#fff; }
/* Gentle pulse (not a spinner icon) makes the loading state unmistakable without adding markup —
   see .is-generating toggled from assets/schedule-pdf.js around the API fetch + PDF build. */
.schedule-modal__download-pdf.is-generating{ animation:schedule-pdf-pulse 1.2s ease-in-out infinite; }
@keyframes schedule-pdf-pulse{ 0%, 100%{ opacity:0.7; } 50%{ opacity:1; } }

/* A dismissible, auto-hiding failure message for the PDF download only (see
   showDownloadPdfError/hideDownloadPdfError in schedule-modal.js) — deliberately a thin banner
   between the header and the schedule body, never replacing the schedule content underneath it. */
.schedule-download-error{
  margin:0; padding:10px 28px;
  background:rgba(156,22,86,0.08); color:#9c1656;
  font-size:0.84rem; font-weight:600; line-height:1.5;
  border-bottom:1px solid rgba(156,22,86,0.18);
}
.schedule-download-error[hidden]{ display:none; }

.schedule-modal__body{
  flex:1 1 auto; min-height:0; overflow-y:auto;
  overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;
  padding:20px 28px 28px;
}

/* Every render of the body's content (initial load, error/retry, and — the reason this exists —
   toggling past lessons on/off) is a full rebuild via container.textContent = "" rather than a
   persistent element changing state, so a CSS *transition* has nothing to animate between; a
   brief fade-in *animation* on each top-level block gives the swap a restrained bit of motion
   instead of an instant, jarring content replacement. */
@keyframes scheduleContentFadeIn{
  from{ opacity:0; }
  to{ opacity:1; }
}
.schedule-modal__body > *{
  animation:scheduleContentFadeIn 180ms ease;
}

/* A quiet, administrator-facing watermark (2026-08) — appended after the rendered schedule content
   only when this modal's data came from the GAS emergency fallback (see
   appendScheduleSourceIndicator() in schedule-modal.js), never for the ordinary static-json source.
   Nothing is ever created for static-json, so there is no hidden element occupying space when the
   indicator doesn't apply — "occupy no visible space when absent" is just "doesn't exist". The row
   wrapper right-aligns the capsule without requiring .schedule-modal__body itself to be a flex
   container (it isn't — it's a plain scrolling block of stacked content). Deliberately not the
   modal header, the Download button, the error banner, or the lesson rows — this is diagnostic
   information about *this dataset*, not a decoration on any specific control. */
.schedule-source-indicator-row{
  text-align:right;
  margin-top:12px;
}
.schedule-source-indicator{
  display:inline-flex;
  align-items:center;

  padding:1px 4px;

  border:1px solid currentColor;
  border-radius:999px;

  color:var(--ink-muted);
  opacity:0.3;

  font-family:var(--sans);
  font-size:6.5px;
  font-weight:600;
  line-height:1.2;
  letter-spacing:0.08em;
  text-transform:uppercase;

  user-select:none;
  pointer-events:none;
}

.schedule-state{ padding:48px 8px; text-align:center; color:var(--ink-muted); font-size:0.98rem; line-height:1.6; }
.schedule-state--error{ color:var(--ink); }
.schedule-state--loading{ padding-top:32px; }
.schedule-state--loading p{ margin:0; }

.schedule-loading-dots{ display:inline-flex; gap:6px; margin-bottom:16px; }
.schedule-loading-dots span{
  width:8px; height:8px; border-radius:50%; background:var(--primary);
  opacity:0.3; animation:scheduleDotPulse 1.1s ease-in-out infinite;
}
.schedule-loading-dots span:nth-child(2){ animation-delay:0.15s; }
.schedule-loading-dots span:nth-child(3){ animation-delay:0.3s; }
@keyframes scheduleDotPulse{
  0%, 80%, 100%{ opacity:0.3; transform:scale(0.82); }
  40%{ opacity:1; transform:scale(1.05); }
}
.schedule-retry-btn{
  display:inline-flex; align-items:center; justify-content:center;
  margin-top:18px; padding:9px 22px; border-radius:999px; border:1px solid var(--primary);
  background:transparent; color:var(--primary); font-weight:700; font-size:0.86rem;
  font-family:var(--sans); cursor:pointer;
  transition:background-color .15s ease;
}
.schedule-retry-btn:hover{ background:var(--primary-soft); }
.schedule-retry-btn:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; }

/* Shown when every published session is in the past — replaces what would otherwise be a
   blank-looking schedule. Reuses .schedule-state's centred/muted treatment; the control to reveal
   the completed schedule lives in the modal header now (.schedule-modal__past-toggle), not here. */
.schedule-state--all-past{ padding-bottom:8px; }

.schedule-part{ margin:0 0 30px; }
.schedule-part:last-child{ margin-bottom:0; }
/* Part landmark (2026-08: number + formal name, not just a number): restrained text plus a single
   thin rule extending to the right — no accent stroke, pill or thick marker. This is the only
   full-width horizontal divider in the schedule; individual lessons are separated by whitespace
   alone (see .schedule-session below), never a line of their own. The rule is a real element
   (.schedule-part-heading__rule), not a ::after, so it sits alongside — not glued to the end of —
   a title whose length now varies with the Part's actual name. */
.schedule-part-heading{
  display:flex; align-items:center; gap:14px;
  margin:0 0 10px;
}
.schedule-part-heading__text{
  display:flex; align-items:baseline; gap:11px;
  min-width:0;
}
.schedule-part-heading__number{
  font-family:var(--display); font-size:0.86rem; font-weight:600;
  color:var(--ink-muted); line-height:1.3; white-space:nowrap;
}
/* 2026-08 softening pass: a muted slate-navy (not var(--ink)'s near-black, and deliberately not
   var(--primary-dark) either — that deep indigo is already spoken for by the lesson date, the
   strongest element in each row, and reusing it here would let the Part name compete with it).
   Sits between the indigo lesson date and the grey metadata text, clearer than a caption but
   still subordinate to the session topic below it. */
.schedule-part-heading__title{
  font-family:var(--display); font-size:0.92rem; font-weight:600;
  color:#394463; line-height:1.45;
}
.schedule-part-heading__rule{ flex:1; min-width:32px; height:1px; background:var(--border); }

html[lang^="zh"] .schedule-part-heading__text{ gap:9px; }

/* Full Part introduction (2026-08 rework: Part-level content, not session-column content) — a
   lightweight full-width panel spanning from the Part heading's own left edge to the session
   topics' right edge (the section's full content width), rather than being indented to start
   under the topic column. Deliberately quiet: pale indigo-grey fill, subtle border, no shadow, no
   accent bar, smaller/lighter text than a session topic — Part-level information framed as
   clearly distinct from, not competing with, the individual sessions below it. The layout here is
   flexbox, not CSS grid, so there is no grid-column spanning to do — a plain full-width block
   already reaches both edges once the old topic-column indent/max-width are removed. */
.schedule-part-intro{
  margin:0.75rem 0 1rem;
  padding:0.75rem 0.95rem;
  border:1px solid #e5e8f2;
  border-radius:10px;
  background:#f6f7fb;
  color:#646978;
  font-size:0.9rem; font-weight:400; line-height:1.65;
  text-align:justify; text-align-last:left; hyphens:none;
}
html[lang^="zh-Hant"] .schedule-part-intro{ line-height:1.75; }

@media (max-width:640px){
  .schedule-part-heading{ align-items:flex-start; margin-bottom:18px; }
  .schedule-part-heading__text{ flex-direction:column; align-items:flex-start; gap:3px; }
  /* The rule reads fine beside a single-line heading on wider viewports, but once the number and
     title stack onto two lines here it visually attaches only to the (now short) first line rather
     than the whole heading block, leaving it looking like a detached fragment beside "Part 1"/
     "第一部分" instead of a divider for the complete heading. Removed outright rather than shortened
     or repositioned — this is the schedule's only full-width divider (see the rule's own comment
     above), and a short floating line at mobile width would read as an even more arbitrary
     fragment, not a fix. The heading's own bottom margin is widened above (10px -> 18px) to keep
     deliberate separation from the session list now that the rule no longer does any of that work. */
  .schedule-part-heading__rule{ display:none; }
  .schedule-part-intro{
    padding:0.7rem 0.8rem;
    text-align:left;
  }
}

.schedule-session-list{ display:flex; flex-direction:column; }

/* No card, border, background or divider on the row itself — lessons are separated by margin
   alone, deliberately larger than the tighter spacing used within a single lesson's own content,
   so the rhythm itself (not a line) marks where one lesson ends and the next begins. */
.schedule-session{
  display:flex; align-items:flex-start; gap:24px;
  margin-bottom:30px;
}
.schedule-session:last-child{ margin-bottom:0; }

/* Left-hand information column: date + weekday + time, grouped together since they answer the
   same question ("when"), separate from the lesson content beside them ("what"). Fixed width so
   every lesson's date column lines up regardless of how long its title runs. */
.schedule-session__info{ flex:0 0 165px; }

/* nowrap (not the earlier flex-wrap:wrap) keeps date + weekday as one compact unit that never
   splits across two lines — the 165px info column is sized with this in mind, so the weekday
   never has to force it wider. */
.schedule-session__date-line{ display:flex; align-items:baseline; flex-wrap:nowrap; white-space:nowrap; }
.schedule-session__date{
  font-family:var(--display); font-weight:700; font-size:1.35rem; line-height:1.25;
  color:var(--primary-dark); font-variant-numeric:tabular-nums;
}
/* Sized relative to the date (0.78em) so it stays clearly readable without ever competing with
   it. Gap to the date is language-specific: English needs a touch more separation between the
   date and its "(Mon)"-style suffix than Chinese does between the date and its full-width （）
   weekday, which otherwise reads as detached — see the html[lang^="zh"] override below. */
.schedule-session__weekday{
  margin-left:5px; font-size:0.78em; font-weight:600; color:var(--ink-muted); line-height:1;
}
html[lang^="zh"] .schedule-session__weekday{ margin-left:2px; }
.schedule-session__time{
  margin-top:6px; font-size:0.94rem; font-weight:400; line-height:1.4;
  color:var(--ink-muted); font-variant-numeric:tabular-nums;
}

.schedule-session__content{ flex:1 1 auto; min-width:0; }

/* Hierarchy within one lesson (2026-08 audit): topic strongest, then the enrolment deadline/
   status pair immediately beneath it (second-most prominent — see .schedule-session__deadline
   below), then speaker and venue/mode as quieter metadata. Speaker and venue share the same
   secondary weight so neither reads as more important than the other. */
.schedule-session__topic{ margin:0 0 7px; font-size:0.98rem; font-weight:600; color:var(--ink); line-height:1.5; text-wrap:pretty; }
.schedule-session__speaker{ margin:0 0 3px; font-size:0.88rem; font-weight:400; color:var(--ink-muted); line-height:1.45; }
/* Quietest tier — venue/mode is metadata, secondary to both the deadline row and the speaker line. */
.schedule-session__meta-line{ margin:0; font-size:0.84rem; font-weight:400; color:var(--ink-faint); line-height:1.5; }

/* Deadline row (2026-08 two-chip refinement): two self-contained chips — deadline (label+date
   together, not a label with the date pushed outside it) and, where applicable, status — rather
   than a label chip followed by a loose date and a separately appended status fragment. Each chip
   wraps as one unit (white-space:nowrap on the chip itself), so narrow viewports drop the status
   chip to its own line rather than breaking either chip's own content. */
.schedule-session__deadline{
  display:flex; align-items:center; flex-wrap:wrap; gap:6px;
  margin:0 0 11px;
}
.schedule-session__chip{
  display:inline-flex; align-items:baseline; gap:4px;
  padding:3px 9px; border-radius:6px;
  font-size:0.82rem; line-height:1.4; white-space:nowrap;
}
/* Amber tint deliberately echoes the enrolment CTA colour (var(--orange) is #ffa628 —
   255,166,40 — the literal rgbas below are that same colour at 14% fill / 40% stroke opacity;
   there's no --orange-rgb triplet token to reference directly). A same-tone stroke (2026-08:
   subtle, not a button outline) anchors the chip's edges more definitely than the fill alone,
   keeping its left edge reading as intentional rather than visually soft next to the topic above
   it. Label and date share this same ink colour AND the same weight, so the whole chip reads as
   one unified block rather than a strong label with a lighter add-on. */
.schedule-session__chip--deadline{
  background:rgba(255,166,40,0.14); color:#7a4a00;
  border:1px solid rgba(255,166,40,0.4);
}
.schedule-session__chip-label{ font-weight:700; }
.schedule-session__chip-value{ font-weight:700; }

/* Status chip — same brighter pink approved for the main SMTP page's Part-level status, now as
   its own compact chip (no separator dot, no free-standing coloured text) sharing the deadline
   chip's height/radius family. Starts at opacity:0/translateY(2px); JS adds is-visible once the
   text is in place (see renderSessionRow), so only this chip ever animates — never the row,
   topic, deadline chip, venue line, Part heading, or modal panel around it. Conveyed through
   wording, not colour alone. */
.schedule-session__chip--status{
  background:rgba(216,27,96,0.12); color:#9c1656; font-weight:600;
  /* 2026-08: brought into line with the amber deadline chip's own stroke treatment above — a
     same-tone border at roughly the same fill-to-stroke opacity ratio (0.12 fill -> 0.35 stroke,
     matching the amber chip's 0.14 -> 0.4), so the two neighbouring chips read as one consistent
     family instead of one outlined and one flat. Still clearly lighter than the solid #9c1656 text
     and adds no padding/height change of its own — it uses the same base .schedule-session__chip
     padding/radius, exactly like the amber chip already does. */
  border:1px solid rgba(216,27,96,0.35);
  opacity:0;
  transform:translateY(2px);
  transition:opacity 180ms ease, transform 180ms ease;
}
.schedule-session__chip--status.is-visible{
  opacity:1;
  transform:translateY(0);
}

/* Past lessons (only ever rendered once "show past lessons" is active — see renderSchedule): a
   subtly reduced visual emphasis on the topic/speaker/meta text, no strikethrough. The date/time
   column is deliberately left untouched so the date stays exactly as legible as an upcoming
   lesson's — only the completed lesson's own content recedes. renderSessionRow never computes an
   enrolment-status for a past session at all (only "Completed" applies), so there is nothing to
   dim here for .schedule-session__chip--status. */
.schedule-session--past .schedule-session__topic{ color:var(--ink-muted); }
.schedule-session--past .schedule-session__speaker,
.schedule-session--past .schedule-session__deadline,
.schedule-session--past .schedule-session__meta-line{ color:var(--ink-faint); opacity:0.75; }
.schedule-session--past .schedule-session__chip--deadline{ color:inherit; background:rgba(0,0,0,0.06); }
.schedule-session__completed-tag{
  display:inline-block; margin-left:2px; padding:1px 8px; border-radius:999px;
  background:var(--bg-alt, #eef0f7); color:var(--ink-faint);
  font-size:0.7rem; font-weight:600; letter-spacing:.01em; vertical-align:middle;
}
/* Plain-text fallback (no map config for the code): a quiet dotted underline just hints at the
   address tooltip, cursor:help signals "hover for more" rather than "click". */
span.schedule-meta-venue{
  text-decoration:underline dotted; text-decoration-color:var(--border); text-underline-offset:2px; cursor:help;
}

/* Linked campus name (opens Google Maps): deliberately not a conventional bright-blue link —
   quiet ink colour at rest, shifting to the brand indigo only on hover/focus. No external-link
   glyph — the underline alone is enough to read as a link. */
a.schedule-meta-venue{
  color:inherit;
  text-decoration:underline dotted; text-decoration-color:var(--border); text-underline-offset:2px;
  cursor:pointer;
  transition:color .15s ease, text-decoration-color .15s ease;
}
a.schedule-meta-venue:hover{ color:var(--primary); text-decoration-color:var(--primary); }
a.schedule-meta-venue:focus-visible{
  outline:2px solid var(--primary); outline-offset:2px; border-radius:2px; color:var(--primary);
}

/* Footnote area: the expandable campus-address list. Deliberately quiet — smaller type, muted
   colour, no card/background — so it reads as reference material rather than competing with the
   schedule itself. */
.schedule-footnote{ margin-top:20px; padding-top:14px; border-top:1px solid var(--border); }

/* Custom disclosure control (see renderScheduleFootnote in schedule-modal.js) — the whole row is
   the button, not just the icon, and aria-expanded/aria-controls are on that same button. Kept
   visually quiet (no background, no bold heavier than the label itself needs) so it still reads
   as secondary to the schedule content above it. */
.venue-toggle{
  display:flex; align-items:center; gap:8px;
  width:100%; min-height:42px;
  padding:6px 4px; margin:0 -4px;
  border:none; border-radius:6px; background:none;
  font-family:var(--sans); font-weight:700; color:var(--ink-muted); font-size:0.82rem;
  text-align:left; cursor:pointer;
}
.venue-toggle:hover{ color:var(--primary); }
.venue-toggle:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; }

.venue-toggle__icon{
  display:inline-flex; flex:0 0 auto; align-items:center; justify-content:center;
  width:13px; height:13px;
  color:var(--ink-faint);
  transition:transform 220ms ease;
}
.venue-accordion.is-open .venue-toggle__icon{ transform:rotate(90deg); }

/* grid-template-rows 0fr -> 1fr animates the panel's height smoothly in both directions without
   ever needing display:none mid-transition; .venue-panel__inner's own overflow:hidden clips its
   content while the row is still collapsing/expanding. Its left padding (icon width 13px + the
   toggle's own 8px gap = 21px) is what lines every venue name and address up under the "V" of
   the label, rather than under the chevron. */
.venue-panel{
  display:grid;
  grid-template-rows:0fr;
  opacity:0;
  transition:grid-template-rows 240ms ease, opacity 180ms ease;
}
.venue-accordion.is-open .venue-panel{
  grid-template-rows:1fr;
  opacity:1;
}
.venue-panel__inner{ overflow:hidden; padding:2px 0 2px 21px; }

.schedule-campus-info__list{ margin:0; font-size:0.84rem; }
.schedule-campus-info__list dt{ font-weight:700; color:var(--ink); margin:10px 0 2px; }
.schedule-campus-info__list dt:first-child{ margin-top:0; }
.schedule-campus-info__list dd{ margin:0; color:var(--ink-muted); line-height:1.55; }
/* Linked address (opens Google Maps) — same quiet-ink-to-indigo treatment as the per-session
   venue link, no icon/arrow glyph; the venue name (dt) above stays plain, unlinked text. */
.schedule-campus-info__list dd a{
  color:inherit;
  text-decoration:underline dotted; text-decoration-color:var(--border); text-underline-offset:2px;
  cursor:pointer;
  transition:color .15s ease, text-decoration-color .15s ease;
}
.schedule-campus-info__list dd a:hover{ color:var(--primary); text-decoration-color:var(--primary); }
.schedule-campus-info__list dd a:focus-visible{
  outline:2px solid var(--primary); outline-offset:2px; border-radius:2px; color:var(--primary);
}

/* Repeats the SMTP page's location note (see .programme-notes__text in proj-smtp.html) at the
   bottom of the modal — always visible regardless of the venue-details disclosure's open/closed
   state (see renderScheduleFootnote), since it sits outside .venue-accordion entirely. Quietest
   tier in the modal, matching .schedule-session__meta-line's weight/colour. */
.schedule-location-note{ margin:14px 0 0; font-size:0.82rem; font-weight:400; color:var(--ink-faint); line-height:1.6; }

/* Enrolment-status brackets appended to the SMTP page's static "Lesson dates" text once the
   Schedule API responds (see enhanceLessonDatesForProgramme in schedule-modal.js) — a compact
   inline addition, not a chip: muted rose text, no background/border/icon, so it reads as part of
   the same editorial line rather than a UI control. Starts at opacity:0/translateY(2px) — JS adds
   is-visible on the next frame once the text is actually in place, so the existing static date
   text is never what's animating, only this new fragment. */
.lesson-dates__status{
  display:inline-block;
  margin-left:0.35em;
  color:#D81B60;
  font-size:0.9em;
  font-weight:600;
  line-height:inherit;
  opacity:0;
  transform:translateY(2px);
  transition:opacity 180ms ease, transform 180ms ease;
}
.lesson-dates__status.is-visible{
  opacity:1;
  transform:translateY(0);
}
/* Traditional Chinese brackets read as directly attached to the date they follow — no gap, unlike
   the EN pattern where a preceding space is part of the natural "(" bracket convention. */
html[lang^="zh"] .lesson-dates__status{ margin-left:0; }

@media (max-width:640px){
  .schedule-modal-overlay{ padding:0; }
  .schedule-modal{
    width:100%; height:100dvh; max-height:none; border-radius:0;
  }
  /* Two rows instead of one: title+Close stay together on the first row, and the past-lessons
     toggle (when present) drops to a second, compact row of its own rather than crowding onto
     the first line with everything else. .schedule-modal__header-actions is unwrapped via
     display:contents so its two children (the toggle and Close) can be placed independently by
     this grid — Close stays in "close" beside the title, the toggle moves to its own full-width
     "toggle" row. When the toggle is hidden (no past lessons), that row has no participating grid
     item in it and simply collapses to zero height — no extra CSS needed for that case. The same
     is true of the "pdf" row below now that the Download button is created [hidden] and only
     revealed once the schedule has actually rendered (2026-08): [hidden]{display:none} removes the
     button (and its own margin-top:10px) from layout entirely while hidden, so there is no blank
     row or residual gap between the title/Close row and whatever comes after it. */
  .schedule-modal__header{
    display:grid;
    grid-template-columns:1fr auto;
    grid-template-areas:"title close" "pdf pdf" "toggle toggle";
    align-items:center;
    column-gap:14px; row-gap:0;
    padding:16px 18px; padding-top:calc(16px + env(safe-area-inset-top, 0px));
  }
  .schedule-modal__header-actions{ display:contents; }
  .schedule-modal__title{ grid-area:title; font-size:1.12rem; }
  /* Drops to its own full-width row below the title, per spec, rather than squeezing onto the
     title/Close row — comes before the (conditional) past-toggle row since it's always present. */
  .schedule-modal__download-pdf{
    grid-area:pdf; justify-self:stretch;
    margin-top:10px; justify-content:center;
  }
  .schedule-modal__past-toggle{
    grid-area:toggle; justify-self:start;
    padding:4px 2px 8px;
  }
  .schedule-modal__close{ grid-area:close; justify-self:end; }
  .schedule-modal__body{
    padding:20px 18px calc(28px + env(safe-area-inset-bottom, 0px));
  }
  .schedule-download-error{ padding:10px 18px; }

  /* No fixed side column at mobile widths — the info column (date/weekday + time, already
     stacked internally exactly as on desktop) simply moves above the lesson content instead,
     with a compact gap before the topic rather than the wider column gap used at desktop. */
  .schedule-session{ flex-direction:column; align-items:stretch; gap:10px; margin-bottom:28px; }
  .schedule-session__info{ flex:0 0 auto; }
}

@media (prefers-reduced-motion:reduce){
  .schedule-modal__close, .schedule-retry-btn, .schedule-modal__past-toggle, .schedule-modal__download-pdf{ transition:none; }
  /* The reveal transform (translateY+scale) is dropped outright rather than just losing its
     transition — with transition:none alone, a reduced-motion visitor would still see the button
     jump straight to its *offset* resting state the instant hidden is cleared, then jump again to
     its final position once .is-ready lands two frames later. Removing transform here means both
     of those jumps land on the same, final position, so there's nothing to visibly jump between. */
  .schedule-modal__download-pdf{ transform:none; }
  .schedule-modal__download-pdf.is-ready{ opacity:1; }
  .schedule-modal__download-pdf.is-generating{ animation:none; opacity:0.7; }
  .schedule-loading-dots span{ animation:none; opacity:0.7; transform:none; }
  .schedule-modal__body > *{ animation:none; }

  /* No JS is waiting on a transitionend event for this accordion (unlike the modal's own
     open/close), so it's safe to drop the transition outright rather than needing the 1ms trick
     used below for the modal panel. */
  .venue-panel{ transition:none; }
  .venue-toggle__icon{ transition:none; }

  /* Keep a very short opacity-only transition rather than removing it outright: the open/close
     JS in schedule-modal.js listens for transitionend on .schedule-modal to know when the exit
     animation has finished, so dropping the transition entirely would make it fall back to its
     safety timeout on every close. 1ms reads as instant to the user while still firing the event. */
  .schedule-modal-overlay,
  .schedule-modal-overlay.is-leaving{ transition:opacity 1ms linear; }
  .schedule-modal,
  .schedule-modal-overlay.is-visible .schedule-modal,
  .schedule-modal-overlay.is-leaving .schedule-modal{
    transition:opacity 1ms linear;
    transform:none;
  }

  .lesson-dates__status{ transition:none; transform:none; }
  .schedule-session__chip--status{ transition:none; transform:none; }
}
