/* AI Engineer Melbourne 2026 — Schedule component.
   Sits inside @layer theme alongside the other /ai-engineer/style/*.css files.

   All tokens are namespaced --schedule-* so they never collide with site
   tokens. Site values come through as fallbacks — the schedule inherits the
   site's typography (no font-* declarations anywhere in this file) and can
   be retuned by overriding any --schedule-* from outside.
*/
@layer theme {

  /* ------------------------------------------------------------
     Tokens — all derived from site tokens with fallbacks.
     Override any of these from outside .schedule to retune.
  ------------------------------------------------------------ */
  .schedule {
    --schedule-ink:        var(--color-ink, #000);
    --schedule-ink-weak:   var(--color-ink-weak, #333);
    --schedule-ink-muted:  var(--color-ink-muted, #4a4a4a);
    --schedule-bg:         var(--color-bg, #fff);
    --schedule-panel:      var(--color-panel, #e5e5e5);
    --schedule-rule:       var(--color-rule, #dddddd);
    --schedule-hairline:   var(--hairline, 1px solid var(--schedule-rule));
    --schedule-accent:     var(--color-accent-3, #000);
    --schedule-on-accent:  var(--color-on-accent, #fff);
    --schedule-link:       var(--link, #4f46e5);

    /* Structural */
    --schedule-max-width:         1400px;
    --schedule-gap:               1rem;
    /* Sticky toolbar's offset from the viewport top.
       On this site the sticky element directly above us is nav.page (the
       per-event header with logo + Register), which tokenises its height
       as --page-nav-height on :root: 64px by default, dropping to 50px
       at <=720px. Referencing that token means our offset follows the
       page-nav responsively — no media query needed here.
       Falls back to 0 on sites that don't define the token, keeping the
       component portable. Override from outside to retune. */
    --schedule-sticky-top:        var(--page-nav-height, 0px);
    /* One time-column width for both views so the left gutter looks
       identical in list and grid. */
    --schedule-time-col:          90px;
    --schedule-time-col-narrow:   60px;
    --schedule-grid-time-col:     var(--schedule-time-col);
    --schedule-speaker-avatar:    28px;
    --schedule-cell-min-height:   1.75rem;
    --schedule-radius:            3px;
    --schedule-radius-pill:       999px;

    /* Track palette — the one place the schedule uses colour.
       Override --schedule-track-N-bg / --schedule-track-N-fg to rebrand. */
    --schedule-track-0-bg: #dbeafe; --schedule-track-0-fg: #1e40af;
    --schedule-track-1-bg: #fce7f3; --schedule-track-1-fg: #9d174d;
    --schedule-track-2-bg: #dcfce7; --schedule-track-2-fg: #166534;
    --schedule-track-3-bg: #fef3c7; --schedule-track-3-fg: #92400e;
    --schedule-track-4-bg: #e0e7ff; --schedule-track-4-fg: #3730a3;
    --schedule-track-5-bg: #fee2e2; --schedule-track-5-fg: #991b1b;
  }

  /* ------------------------------------------------------------
     Root container
  ------------------------------------------------------------ */
  .schedule {
    max-width: var(--schedule-max-width);
    margin-inline: auto;
    padding-inline: 1rem;
    color: var(--schedule-ink);
  }

  .schedule a { color: var(--schedule-link); }

  .schedule-day-heading {
    margin-block: 2rem 0.75rem;
    padding-block-end: 0;
    /* Hard kill any border-bottom inherited from a generic h2 rule on the
       host page (the dev shell's h2 has one; production typography may
       too). The first row of content (or the grid table's own border-top)
       provides the divider. */
    border-bottom: none;
    /* The site's h2 rule sizes this at --font-size-x-large (~2.25-3rem),
       which is too loud over a compact session list. Step it down to
       --font-size-large. Centre the heading so it reads as a day label
       rather than a section title competing with the cards below. */
    font-size: var(--font-size-large);
    text-align: center;
    /* Anchor-jump offset — when #schedule-day-<date> is the scroll target
       (either from a toolbar day pill or an external link), scroll the
       heading to just below the sticky stack, not flush against the top
       where it would disappear behind the page-nav + toolbar. The JS
       measures the actual stack height and publishes it via the
       --schedule-scroll-offset custom property on .schedule; we fall
       back to a rough estimate when JS hasn't run yet. */
    scroll-margin-top: var(--schedule-scroll-offset, 8rem);
  }

  /* ------------------------------------------------------------
     View switching

     The fragment renders BOTH the list and grid views into the DOM.
     The section's [data-view] attribute decides which is visible —
     the toggle JS flips that attribute to swap views instantly, with
     no network round-trip. Without JS, the toggle links fall back to
     a full-page navigation (?schedule_view=list|grid), and the server
     sets [data-view] accordingly on first render.
  ------------------------------------------------------------ */
  .schedule .schedule-view { display: none; }
  .schedule[data-view="list"] .schedule-view[data-schedule-view="list"],
  .schedule[data-view="grid"] .schedule-view[data-schedule-view="grid"] {
    display: block;
  }

  /* ------------------------------------------------------------
     Toolbar — view toggle + day nav, one horizontal row, sticky

     The toolbar groups both control clusters so they share a single
     pinned bar at the top of the viewport. The background is opaque so
     scrolled content doesn't bleed through. z-index keeps it above the
     grid cell backgrounds that share the same stacking context.
  ------------------------------------------------------------ */
  .schedule-toolbar {
    position: sticky;
    top: var(--schedule-sticky-top);
    /* Stay below the host site's sticky bars so they layer correctly
       over us. On this site the stack is site-nav (z: 200, dropdowns
       z: 400) > page-nav (z: 120) > us (z: 10). Above ordinary flow
       content so our bar stays visible while scrolling the schedule. */
    z-index: 10;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1.25rem;
    margin-block: 0.75rem 1rem;
    padding-block: 0.6rem;
    background: var(--schedule-bg);
    /* Hairline underline separates sticky bar from the scrolling content
       once the bar is pinned. Also visible at rest — cheap delimiter. */
    border-bottom: var(--schedule-hairline);
    /* No accidental text selection when clicking / double-clicking the
       pills. The individual pill rules also set user-select: none but
       adding it here guarantees the label spans inside the pills — and
       any whitespace between pills — can't be selected either. */
    user-select: none;
    -webkit-user-select: none;
  }

  /* Both control clusters lay out with the same flex-row + gap, so the
     four buttons visually belong to one continuous bar. The view toggle
     sits on the left, day nav on the right (margin-inline-start: auto). */
  .schedule-view-toggle,
  .schedule-day-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0;
  }
  /* Day nav sits on the LEFT of the toolbar, before the view-toggle
     cluster. Achieved with flex `order: -1` so we don't have to
     reorder the DOM (which would also affect tab order / screen-
     reader flow — keeping the source order means assistive tech
     still hears view-toggle → search → days, which is the logical
     reading order). The search trigger takes the remaining
     horizontal space and pushes the agent button to the right. */
  .schedule-day-nav {
    order: -1;
    margin-inline-start: 0;
  }
  .schedule-search-trigger {
    /* Push everything after the search box to the right edge. */
    margin-inline-end: auto;
  }

  /* Unified pill styling — all controls in the toolbar look the same:
     the two view-toggle links, the compact checkbox, and the day-nav
     links. The checkbox's <input> is visually hidden; the <label> is
     the pill the user sees and clicks. */
  .schedule-view-toggle a,
  .schedule-day-nav a,
  .schedule-compact-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.7rem;
    border: var(--schedule-hairline);
    border-radius: var(--schedule-radius-pill);
    text-decoration: none;
    color: var(--schedule-ink);
    /* Toolbar pills used to inherit body type. Step them down a
       notch so the toolbar reads as supporting chrome, not as part
       of the schedule content. Smaller text also lets the row pack
       more pills before wrapping. */
    font-size: var(--font-size-small);
    line-height: 1.2;
    cursor: pointer;
    user-select: none;
    /* Keep each pill at its intrinsic width — the toolbar's flex layout
       wraps rather than shrinks. Without this, narrowing the window
       below the combined natural width of all pills squashes them
       progressively instead of spilling the last one to a new line. */
    flex: none;
    /* And keep the label text itself on a single line. Without nowrap,
       "View by time" wrapped to three stacked lines inside a narrow
       pill — the label has room to wrap because the pill is flex-none
       but the container is constrained; better to let the pill keep its
       single-line intrinsic width and let the toolbar wrap whole pills
       to a new row as needed. */
    white-space: nowrap;
  }

  /* Inline SVG icon tucked inside view-toggle / compact pills. Inherits
     the pill's current text colour (so active-state + hover recolour it
     along with the label). 1em sizing means it tracks the text's font
     size if we ever adjust the pill typography. */
  .schedule-icon {
    width: 1em;
    height: 1em;
    flex: none;
    color: inherit;
  }

  /* Icon-only pills: View by time / View by track / Compact. The text
     label is still in the DOM (inside .schedule-iconbtn-label) so
     screen readers announce it, and the native `title` attribute
     provides a hover tooltip. A slightly bigger icon (1.1em) and more
     padding make the tap target feel equivalent to the day pills beside
     them, even without the visible label.

     `flex: none` keeps each pill at its intrinsic width. Without it the
     toolbar's flex container can shrink the pills until the icon is
     squashed against the border — visible at middle widths where the
     day pills are still present but there's not quite enough room. We
     want the pills to wrap to a new line, not implode. */
  .schedule-iconbtn {
    padding-inline: 0.7rem;
    flex: none;
  }
  .schedule-iconbtn .schedule-icon {
    width: 1.1em;
    height: 1.1em;
  }

  /* Robot pill — same shape as the view/compact pills, but it's not a
     mode toggle: it links out to the developer/agent landing page.
     Push it slightly apart from the toggles with a left margin so it
     reads as "different family". `margin-inline-start: auto` would
     shove it to the far right of the cluster, which is too far given
     the day pills already have right-alignment. A fixed gap is enough
     visual separation. */
  .schedule-iconbtn--agent {
    margin-inline-start: 0.75rem;
  }

  /* ------------------------------------------------------------
     Search trigger — looks like a search input, behaves as a button.
     Sits in the middle of the toolbar between the icon-pill cluster
     and the day-nav, taking the available horizontal space. Click
     opens the dialog; "/" keyboard shortcut also opens it. Once
     the user has searched, the label updates to show the current
     query (with .has-query darkening it from placeholder grey to
     ink colour) so they can see at a glance what they're filtering
     by, even with the dialog closed.
  ------------------------------------------------------------ */
  .schedule-search-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.7rem;
    border: var(--schedule-hairline);
    border-radius: var(--schedule-radius-pill);
    background: var(--schedule-bg);
    color: var(--schedule-ink-muted);
    font: inherit;
    /* Match the smaller pill text — search trigger sits visually
       alongside the view-toggle / day pills and should read at the
       same size. */
    font-size: var(--font-size-small);
    line-height: 1.2;
    cursor: pointer;
    user-select: none;
    text-align: left;
    /* Take the available middle space, but don't dominate. At
       narrow widths the toolbar wraps and this gets a full row. */
    flex: 1 1 14rem;
    min-width: 11rem;
    max-width: 26rem;
  }
  /* Hover: own both background and text colour so the host site's
     own button-hover rules can't strand black text on a purple
     background. White text + ink-on-link badge bg keeps the kbd
     hint readable too. */
  .schedule-search-trigger:hover,
  .schedule-search-trigger:focus-visible {
    background: var(--schedule-link);
    border-color: var(--schedule-link);
    color: #fff;
  }
  .schedule-search-trigger:hover .schedule-search-trigger-key,
  .schedule-search-trigger:focus-visible .schedule-search-trigger-key {
    background: rgba(255, 255, 255, 0.22);
    color: #fff;
  }
  .schedule-search-trigger:focus-visible {
    outline: 2px solid var(--schedule-link);
    outline-offset: 2px;
  }
  .schedule-search-trigger .schedule-icon {
    width: 1.1em;
    height: 1.1em;
    flex: none;
  }
  .schedule-search-trigger-label {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .schedule-search-trigger.has-query .schedule-search-trigger-label {
    color: var(--schedule-ink);
    font-weight: var(--weight-medium, 600);
  }
  /* Hover/focus override for the .has-query label — without these,
     the explicit colour above beats the parent's hover colour
     change (specificity tie, but it appears later in the cascade).
     Placed after .has-query so source order also tips the cascade
     toward white. */
  .schedule-search-trigger:hover .schedule-search-trigger-label,
  .schedule-search-trigger:focus-visible .schedule-search-trigger-label,
  .schedule-search-trigger.has-query:hover .schedule-search-trigger-label,
  .schedule-search-trigger.has-query:focus-visible .schedule-search-trigger-label {
    color: #fff;
  }
  .schedule-search-trigger-key {
    flex: none;
    font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;
    font-size: 0.78em;
    background: var(--schedule-panel);
    color: var(--schedule-ink-muted);
    padding: 0.05rem 0.4rem;
    border-radius: 3px;
    line-height: 1.2;
  }

  /* ------------------------------------------------------------
     Search dialog

     Native <dialog>, opened with .showModal() so we get backdrop +
     focus trap. Centred near the top of the viewport so results
     have natural reading flow downward. The dialog itself is a
     column: header (icon + input + close), meta row (count +
     keyboard hint), scrollable results list, empty state.

     Note on heights: we don't set `height` or use `flex: 1` on the
     results list, because that interacts badly with the dialog's
     auto-sized height — the list collapses to 0px when the dialog
     is shorter than max-height. Instead the list sizes to its
     content, capped by max-height + overflow on the list itself.
  ------------------------------------------------------------ */
  .schedule-search-dialog {
    width: min(820px, 94vw);
    margin: 6vh auto 0;
    padding: 0;
    border: 1px solid var(--schedule-rule);
    border-radius: 10px;
    background: var(--schedule-bg);
    color: var(--schedule-ink);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);
    overflow: hidden;
  }
  .schedule-search-dialog::backdrop {
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(2px);
  }
  /* Header row — icon, input, close button. Input is borderless
     because the dialog frame itself is the visual container. */
  .schedule-search-header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.85rem 1rem;
    border-bottom: var(--schedule-hairline);
  }
  .schedule-search-icon {
    width: 1.15em;
    height: 1.15em;
    color: var(--schedule-ink-muted);
    flex: none;
  }
  .schedule-search-input {
    flex: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    font: inherit;
    font-size: 1.05rem;
    color: inherit;
    padding: 0;
  }
  .schedule-search-input::placeholder {
    color: var(--schedule-ink-muted);
  }
  /* Close button — circular hit area with the × glyph centred.
     Lock down box-sizing + padding + aspect-ratio so the host
     WordPress theme's button styles (which often add padding on
     hover) can't deform the circle into an ellipse. Flex + center
     keeps the glyph centred regardless of font metrics. */
  .schedule-search-close {
    flex: none;
    box-sizing: border-box;
    width: 1.8rem;
    height: 1.8rem;
    aspect-ratio: 1;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--schedule-ink-muted);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .schedule-search-close:hover,
  .schedule-search-close:focus-visible {
    background: var(--schedule-panel);
    color: var(--schedule-ink);
    /* Re-assert the dimensions in case the host theme reshapes
       buttons on hover via padding-changes. */
    padding: 0;
    width: 1.8rem;
    height: 1.8rem;
    outline: none;
  }
  /* Tabs row — Text / Semantic mode toggle, with the result count
     pinned to the right. The tabs themselves look like quiet pills;
     active tab gets the link colour to read as "selected".
     Click switches mode and re-runs the query in the new backend. */
  .schedule-search-tabs {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.45rem 1rem;
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    border-bottom: var(--schedule-hairline);
  }
  .schedule-search-tab {
    appearance: none;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--schedule-radius-pill);
    padding: 0.18rem 0.7rem;
    font: inherit;
    color: var(--schedule-ink-muted);
    cursor: pointer;
    line-height: 1.3;
  }
  /* Own both background and colour on hover/focus — without the
     explicit background, the host WordPress theme's button-hover
     rules can paint a purple background under our muted-grey text,
     leaving it unreadable. White-on-link gives the same affordance
     family as the toolbar Search trigger. */
  .schedule-search-tab:hover,
  .schedule-search-tab:focus-visible {
    background: var(--schedule-link);
    border-color: var(--schedule-link);
    color: #fff;
    outline: none;
  }
  .schedule-search-tab.is-active {
    color: var(--schedule-link);
    border-color: var(--schedule-link);
    font-weight: var(--weight-medium, 600);
  }
  /* Active tab on hover keeps the active-tab look (outlined indigo)
     rather than flipping to the filled-purple hover state — feels
     wrong to "highlight" the already-selected tab as if it were a
     new action. */
  .schedule-search-tab.is-active:hover,
  .schedule-search-tab.is-active:focus-visible {
    background: transparent;
    color: var(--schedule-link);
    border-color: var(--schedule-link);
  }
  .schedule-search-meta {
    margin-inline-start: auto;
  }
  .schedule-search-count {
    font-variant-numeric: tabular-nums;
  }
  /* Results list — sizes to content up to a viewport-relative cap,
     scrolls internally past that. NO `flex: 1` (that collapsed the
     list to 0px in the previous version, which is why nothing
     showed up even though the count read "5 results"). */
  .schedule-search-results {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    max-height: min(60vh, 540px);
  }
  .schedule-search-result {
    display: grid;
    grid-template-columns: 4.5rem 1fr auto;
    gap: 0.75rem;
    align-items: center;
    padding: 0.65rem 1rem;
    border-bottom: var(--schedule-hairline);
    cursor: pointer;
  }
  .schedule-search-result:last-child {
    border-bottom: none;
  }
  .schedule-search-result.is-active {
    background: var(--schedule-panel);
  }
  .schedule-search-result-when {
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
  }
  .schedule-search-result-day {
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: var(--weight-medium, 600);
  }
  .schedule-search-result-time {
    color: var(--schedule-ink);
    font-size: var(--font-size-small);
  }
  .schedule-search-result-main {
    min-width: 0;
  }
  .schedule-search-result-title {
    font-size: var(--font-size-regular);
    font-weight: var(--weight-medium, 600);
    line-height: 1.3;
    overflow-wrap: anywhere;
  }
  .schedule-search-result-speakers {
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    margin-top: 0.15rem;
  }
  /* Right-side aside on each result row — stacks track + (semantic
     mode only) similarity score vertically. Lets us add the score
     without restructuring the grid. */
  .schedule-search-result-aside {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.25rem;
  }
  .schedule-search-result-track {
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    white-space: nowrap;
  }
  /* Confidence badge — small, tabular numerals so the column
     stays vertically aligned across rows. Faint background so it
     reads as a chip rather than just text. */
  .schedule-search-result-score {
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    font-variant-numeric: tabular-nums;
    background: var(--schedule-panel);
    padding: 0.05rem 0.4rem;
    border-radius: var(--schedule-radius);
    white-space: nowrap;
  }
  .schedule-search-empty,
  .schedule-search-loading,
  .schedule-search-error {
    padding: 1.5rem 1rem;
    margin: 0;
    text-align: center;
    font-size: var(--font-size-small);
    color: var(--schedule-ink-muted);
  }
  .schedule-search-error {
    color: #b91c1c;
  }

  /* Brief flash on the card we just jumped to from the search
     dialog — a soft outline that fades, so the user can see what
     they landed on without the page feeling jolted. */
  .schedule-session.is-jumped-to {
    animation: schedule-jump-flash 1.4s ease;
  }
  @keyframes schedule-jump-flash {
    0%   { outline: 2px solid transparent; outline-offset: 2px; }
    20%  { outline: 2px solid var(--schedule-link); outline-offset: 4px; background: rgba(79, 70, 229, 0.06); }
    100% { outline: 2px solid transparent; outline-offset: 2px; background: transparent; }
  }
  @media (prefers-reduced-motion: reduce) {
    .schedule-session.is-jumped-to { animation: none; }
  }

  /* Narrow viewport — dialog spans more of the screen, smaller
     padding inside result rows so each fits more density. */
  @media (max-width: 600px) {
    .schedule-search-dialog {
      width: 96vw;
      margin-top: 3vh;
      max-height: 90vh;
    }
    .schedule-search-result {
      grid-template-columns: 3.5rem 1fr;
    }
    .schedule-search-result-track {
      grid-column: 1 / -1;
      padding-left: 4.25rem;
      margin-top: 0.1rem;
    }
  }

  /* Icon-button text label. Default (narrow viewports): visually hidden
     but still in the accessibility tree for screen readers + linters.
     The pill shows the icon only, with `title` attribute as tooltip.
     At ≥900px we reveal the label as regular text, so wide viewports
     get the full "View by time" / "View by track" / "Compact" wording.
     Labels sit next to the icon via the pill's own `gap: 0.4rem`. */
  .schedule-iconbtn-label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  @media (min-width: 900px) {
    .schedule-iconbtn-label {
      position: static;
      width: auto; height: auto;
      margin: 0; padding: 0;
      overflow: visible;
      clip: auto;
      clip-path: none;
      white-space: normal;
    }
    /* Matching padding for icon+label at wide widths — bring it back up
       to the day-pill padding so all four pills in the toolbar read as
       the same control family. */
    .schedule-iconbtn {
      padding-inline: 0.85rem;
    }
  }

  .schedule-view-toggle a:hover,
  .schedule-day-nav a:hover,
  .schedule-compact-toggle:hover {
    border-color: var(--schedule-link);
    color: var(--schedule-link);
  }

  .schedule-view-toggle a.is-active,
  .schedule-compact-toggle:has(.schedule-compact-switch:checked) {
    /* Outlined treatment in the link colour — visually clear without
       dropping a heavy black blob into the page (the dev shell maps
       --schedule-accent to black, which dominates everything).
       No font-weight change: when the label is visible at wide widths,
       switching from 400 → 600 on selection widens the pill by a few px
       and shifts every pill beside it. Colour + border gives the same
       "selected" read without the layout jitter. */
    background: transparent;
    color: var(--schedule-link);
    border-color: var(--schedule-link);
  }

  /* Visually hide the checkbox but keep it focusable / screen-reader
     accessible. The label's :has() selector above surfaces the checked
     state as the visible outline. A focus ring on the label when the
     input is focused keeps the keyboard affordance. */
  .schedule-compact-switch {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .schedule-compact-toggle:has(.schedule-compact-switch:focus-visible) {
    outline: 2px solid var(--schedule-link);
    outline-offset: 2px;
  }

  /* ------------------------------------------------------------
     Session card

     Rendered identically in both views — same markup, same applied CSS.
     Typography is expressed via site tokens (--font-size-*, --weight-*)
     so the cards inherit the sub-site's scale and stay compact against
     the body copy around them.
  ------------------------------------------------------------ */
  .schedule-session { padding-block: 0.4rem; }

  .schedule-title {
    margin: 0;
    /* The site's h3 rule sizes this at --font-size-large (~1.25-1.5rem)
       which is too loud for a card title. Step down to body size and
       rely on weight-medium for hierarchy. */
    font-size: var(--font-size-regular);
    font-weight: var(--weight-medium, 600);
    line-height: 1.3;
    /* Allow long unbreakable strings (URLs, model names like "gpt-5-codex")
       to wrap inside narrow grid cells instead of overflowing the column. */
    overflow-wrap: anywhere;
    word-break: break-word;
  }

  .schedule-session--break .schedule-title {
    color: var(--schedule-ink-muted);
  }

  .schedule-type {
    display: inline-block;
    font-size: var(--font-size-x-small);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--schedule-ink-muted);
    margin-inline-end: 0.4rem;
  }

  .schedule-desc {
    margin-block: 0.35rem 0;
    font-size: var(--font-size-small);
    color: var(--schedule-ink-weak);
    /* Cap the reading measure. Without this, descriptions in the list
       view stretch to the full row width (~1300px on a wide screen),
       which is uncomfortable for body text. */
    max-width: 65ch;
  }
  /* Venue / room label — small, muted, sits below the description
     (or below the title for break/social rows without a description).
     Used for sessions where the room varies (Registration in the
     foyer, opening in Cinema 1, etc.). Talks all run in their
     respective track venue, so this often just reinforces what the
     track chip already says. */
  .schedule-location {
    margin-block-start: 0.35rem;
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  /* Description body contains rendered markdown. Override the
     host WP theme's generic <p> margin-bottom (typically 1em or
     more) — we want paragraphs in a card to feel tight, not like
     prose blocks on a blog post. !important needed because the
     host's selectors are often more specific than ours by virtue
     of being deeper in the cascade or scoped to broader contexts.
     This is the one place I make peace with !important: trying to
     out-specificity an unknown host theme is an unwinnable game.

     Why a small margin at all and not zero: descriptions
     occasionally do have multiple genuine paragraphs and we want
     them to be readable. 0.25rem is enough breathing room without
     the "wait, why is this on its own line" look that bigger
     gaps produce when the source markdown has accidental blank
     lines around an inline link. */
  .schedule-desc p {
    margin: 0 0 0.25rem !important;
    padding: 0 !important;
  }
  .schedule-desc p:last-child {
    margin-bottom: 0 !important;
  }
  /* Inline link styling. The `display: inline` is defensive — if
     the host theme has a global `a { display: block }` (e.g. a
     reset stylesheet that styles all anchors as block-level for
     navigation), it would otherwise force this link onto its own
     line, exaggerating the paragraph-break problem. We also tell
     the link not to break across lines awkwardly via word-break.
     Underline on hover only so the description prose stays
     readable. */
  .schedule-desc a {
    display: inline;
    color: var(--schedule-link);
    text-decoration: none;
    border-bottom: 1px solid currentColor;
    border-bottom-color: transparent;
    transition: border-color 120ms ease;
    word-break: normal;
  }
  .schedule-desc a:hover,
  .schedule-desc a:focus-visible {
    border-bottom-color: currentColor;
  }
  .schedule-desc code {
    background: var(--schedule-panel);
    padding: 0.05rem 0.3rem;
    border-radius: var(--schedule-radius);
    font-size: 0.95em;
  }
  .schedule-desc strong { font-weight: var(--weight-medium, 600); }

  /* "You might also like" — small chips beneath each session card,
     pulled from the related_session_ids list that's pre-computed at
     embeddings-rebuild time (top-5 cosine-similar peers per session
     using Workers AI BGE 768-dim). The chip is an <a> for native
     keyboard accessibility + no-JS fallback (href jumps to the day);
     JS intercepts the click and scrolls to the specific session. */
  .schedule-related {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem 0.5rem;
    margin-block: 0.5rem 0.25rem;
    /* Cap = the smaller of "comfortable reading width" and "fits
       in my parent". Without the 100%, a wide chip in a narrow
       grid cell could push the wrapper out into the next column. */
    max-width: min(65ch, 100%);
  }
  .schedule-related-label {
    font-size: var(--font-size-x-small);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--schedule-ink-muted);
    flex: none;
  }
  .schedule-related-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.2rem 0.7rem 0.2rem 0.55rem;
    border: 1px solid var(--schedule-rule);
    border-radius: var(--schedule-radius-pill);
    background: var(--schedule-bg);
    color: var(--schedule-ink);
    text-decoration: none;
    font-size: var(--font-size-x-small);
    line-height: 1.3;
    /* Cap = the smaller of "comfortable for reading" and "fits in
       my parent column". 22rem caps width in list view where the
       row body is wide; 100% prevents bleed into the next track
       column in the grid view where each cell is narrower. The
       title's ellipsis (set on .schedule-related-title) truncates
       cleanly when 100% wins. */
    max-width: min(22rem, 100%);
    /* Belt-and-braces: in case some browser ignores max-width: min(),
       a hard min-width: 0 lets the flex parent shrink the chip
       below its intrinsic content width. */
    min-width: 0;
    /* `position: relative` anchors the hover popover to this chip. */
    position: relative;
  }
  .schedule-related-chip:hover,
  .schedule-related-chip:focus-visible {
    border-color: var(--schedule-link);
    color: var(--schedule-link);
    text-decoration: none;
  }
  .schedule-related-when {
    color: var(--schedule-ink-muted);
    font-variant-numeric: tabular-nums;
    flex: none;
    white-space: nowrap;
  }
  .schedule-related-chip:hover .schedule-related-when,
  .schedule-related-chip:focus-visible .schedule-related-when {
    color: var(--schedule-link);
  }
  /* Title shrinks + ellipsises inside the flex chip. Without
     `min-width: 0` the flex item refuses to shrink below content
     width and the ellipsis never triggers — the chip just clips. */
  .schedule-related-title {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Hover/focus popover — appears below the chip with full title,
     speaker line, and a description snippet. CSS-only show/hide
     (no JS) so it Just Works on any keyboard or pointer. Pointer
     events disabled when hidden so it doesn't catch stray clicks
     or block the chip itself. */
  .schedule-related-pop {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    width: max-content;
    max-width: min(360px, 90vw);
    padding: 0.65rem 0.8rem;
    background: var(--schedule-bg);
    color: var(--schedule-ink);
    border: 1px solid var(--schedule-rule);
    border-radius: 8px;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
    font-size: var(--font-size-x-small);
    line-height: 1.4;
    text-align: left;
    /* Stack the inner spans vertically. */
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    /* Hidden by default; revealed via :hover/:focus-within on the
       chip. opacity + visibility transition for a soft fade. */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(-4px);
    transition: opacity 130ms ease, visibility 130ms ease,
                transform 130ms ease;
    z-index: 50;
  }
  .schedule-related-chip:hover .schedule-related-pop,
  .schedule-related-chip:focus-visible .schedule-related-pop {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    /* Keep pointer-events off so the popover doesn't intercept the
       click-on-chip path. Hover covers desktop, focus-visible
       covers keyboard nav. Mobile users tap → jumpTo (no popover
       needed there anyway). */
  }
  /* If the chip is in the right half of its container, the popover
     would otherwise overflow the right edge. Lazy fix: when the
     chip is closer to the right edge than to the left, anchor the
     popover to the chip's right edge instead. We approximate this
     with :nth-last-child for the LAST chip in a row — most cases. */
  .schedule-related-chip:last-child .schedule-related-pop {
    left: auto;
    right: 0;
  }
  .schedule-related-pop-title {
    font-size: var(--font-size-small);
    font-weight: var(--weight-medium, 600);
    line-height: 1.3;
    color: var(--schedule-ink);
  }
  .schedule-related-pop-meta {
    color: var(--schedule-ink-muted);
    font-variant-numeric: tabular-nums;
  }
  .schedule-related-pop-speakers {
    color: var(--schedule-ink-weak);
  }
  .schedule-related-pop-desc {
    color: var(--schedule-ink-weak);
    line-height: 1.45;
  }

  /* ------------------------------------------------------------
     Themes view — sessions grouped by primary theme, computed at
     embeddings-rebuild time via cosine similarity to curator-
     defined theme descriptions (see services/conferences.ts).
     Drops the time/track structure entirely; the question this
     view answers is "what is this conference about?"
  ------------------------------------------------------------ */
  .schedule-theme-section {
    margin-block: 2.25rem;
  }
  .schedule-theme-section:first-child {
    margin-block-start: 1rem;
  }
  .schedule-theme-heading {
    margin-block: 0 0.85rem;
    padding-block-end: 0.4rem;
    border-bottom: var(--schedule-hairline);
    font-size: var(--font-size-large);
    font-weight: var(--weight-medium, 600);
    text-align: left;
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
  }
  .schedule-theme-count {
    font-size: var(--font-size-small);
    font-weight: 400;
    color: var(--schedule-ink-muted);
  }
  /* Card grid — auto-fill columns at ~18rem each, wrapping
     naturally. Keeps compact density at wide widths and stacks
     to one column on phones. */
  .schedule-theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
    gap: 0.65rem;
  }
  /* Each card is a clickable affordance — block link styled as a
     small panel. Click → switch back to list view + jumpTo the
     session (handled in JS). */
  .schedule-theme-card {
    display: block;
    padding: 0.65rem 0.8rem;
    border: var(--schedule-hairline);
    border-radius: 6px;
    background: var(--schedule-bg);
    color: inherit;
    text-decoration: none;
    transition: border-color 120ms ease, background 120ms ease;
  }
  .schedule-theme-card:hover,
  .schedule-theme-card:focus-visible {
    border-color: var(--schedule-link);
    text-decoration: none;
  }
  .schedule-theme-card-title {
    font-size: var(--font-size-regular);
    font-weight: var(--weight-medium, 600);
    line-height: 1.3;
    color: var(--schedule-ink);
    overflow-wrap: anywhere;
  }
  .schedule-theme-card-speakers {
    margin-block-start: 0.25rem;
    font-size: var(--font-size-small);
    color: var(--schedule-ink-muted);
  }
  .schedule-theme-card-meta {
    margin-block-start: 0.5rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 0.4rem;
    align-items: center;
  }
  /* Secondary-theme tags — quieter than the track chip so the
     primary visual hierarchy is title → speakers → track. */
  .schedule-theme-tag {
    display: inline-block;
    padding: 0.05rem 0.45rem;
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    background: transparent;
    border: 1px solid var(--schedule-rule);
    border-radius: var(--schedule-radius);
    text-transform: none;
    letter-spacing: 0;
    line-height: 1.4;
  }

  /* View visibility — extend the existing two-view rule to also
     reveal themes. */
  .schedule[data-view="themes"] .schedule-view[data-schedule-view="themes"] {
    display: block;
  }
  /* And hide the per-day headings in themes view — themes is a
     flat-by-topic listing, not day-grouped. */
  .schedule[data-view="themes"] .schedule-day-heading {
    display: none;
  }
  /* Hide related chips entirely in compact mode — the whole point
     of compact is "less stuff", and these are an extra layer. */
  .schedule[data-compact="true"] .schedule-related {
    display: none;
  }

  /* Speaker byline */
  .schedule-speakers {
    margin-block: 0.35rem 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem 0.8rem;
  }

  .schedule-speaker {
    display: flex;
    align-items: center;
    gap: 0.4rem;
  }

  .schedule-speaker-photo {
    width: var(--schedule-speaker-avatar);
    height: var(--schedule-speaker-avatar);
    border-radius: 50%;
    object-fit: cover;
    background: var(--schedule-panel);
  }

  .schedule-speaker-name {
    font-size: var(--font-size-small);
    line-height: 1.2;
  }
  .schedule-speaker-job {
    font-size: var(--font-size-x-small);
    color: var(--schedule-ink-muted);
    line-height: 1.2;
  }

  /* ------------------------------------------------------------
     Track chips — coloured pill per track
  ------------------------------------------------------------ */
  .schedule-track {
    display: inline-block;
    margin-inline-end: 0.4rem;
    padding: 0.05rem 0.5rem;
    font-size: var(--font-size-x-small);
    font-weight: var(--weight-medium, 600);
    line-height: 1.6;
    border-radius: var(--schedule-radius);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    background: var(--schedule-panel);
    color: var(--schedule-ink);
  }

  .schedule-track.schedule-track-0 { background: var(--schedule-track-0-bg); color: var(--schedule-track-0-fg); }
  .schedule-track.schedule-track-1 { background: var(--schedule-track-1-bg); color: var(--schedule-track-1-fg); }
  .schedule-track.schedule-track-2 { background: var(--schedule-track-2-bg); color: var(--schedule-track-2-fg); }
  .schedule-track.schedule-track-3 { background: var(--schedule-track-3-bg); color: var(--schedule-track-3-fg); }
  .schedule-track.schedule-track-4 { background: var(--schedule-track-4-bg); color: var(--schedule-track-4-fg); }
  .schedule-track.schedule-track-5 { background: var(--schedule-track-5-bg); color: var(--schedule-track-5-fg); }

  /* ------------------------------------------------------------
     List view — one session per row with time in left gutter
  ------------------------------------------------------------ */
  .schedule-row-list {
    display: grid;
    grid-template-columns: var(--schedule-time-col) 1fr;
    /* Top-aligned: with cards now of very varied heights (some have
       a long description + location + speaker, some are one-line
       breaks), centring the time against the card would float "10:00"
       to the vertical midpoint of a tall card — visually disconnected
       from both the title and the chip. Top-aligned keeps the time
       beside the title for every card, regardless of length. */
    align-items: start;
    gap: var(--schedule-gap);
    padding-block: 0.5rem;
  }
  /* Hairline between adjacent rows — mirrors the grid view's
     `.schedule-grid-slot + .schedule-grid-slot { border-top }` pattern so
     both views have exactly the same rule rhythm: no rule above the
     first row, a single hairline between each pair, no rule below the
     last row. Previously list used `border-bottom` on every row, which
     painted a rule above the footer that grid view didn't have. */
  .schedule-row-list + .schedule-row-list {
    border-top: var(--schedule-hairline);
  }

  /* Break rows (registration / lunch / housekeeping) use the SAME row
     spacing as regular sessions — no extra margin, no extra padding, no
     extra border. The differentiation is purely typographic: muted title,
     no description, type chip saying HOUSEKEEPING/SOCIAL. Matching the
     spacing is what makes list-view and grid-view render at the same
     vertical rhythm; previously the extra 2rem of padding around breaks
     in the list view pushed every subsequent row down, so switching to
     the grid view visibly jumped the whole schedule up. */

  .schedule-time {
    /* Match the chip's typography so the two visually top-align on
       the same line. The chip uses --font-size-x-small with uppercase
       transform and tight line-height; the time has historically used
       the larger --font-size-small with default line-height, which
       made its top edge sit higher than the chip top — reads as
       misaligned. Matching font-size and line-height eliminates the
       cap-height discrepancy without resorting to ad-hoc padding
       tweaks that need re-tuning whenever fonts change. */
    font-size: var(--font-size-small);
    line-height: 1.4;
    color: var(--schedule-ink-muted);
    font-variant-numeric: tabular-nums;
    /* No padding tweaks — align-items: center on the row handles
       vertical placement against the card. */
  }

  .schedule-time-end {
    display: block;
    opacity: 0.7;
  }

  /* ------------------------------------------------------------
     Grid view — parallel tracks across columns, time down rows

     Only horizontal rules: one between time slots. No vertical rules
     between track columns — the coloured track chip on each card does
     the per-column labelling, and vertical hairlines made the grid feel
     like a database table.

     Structure:
       .schedule-grid
         .schedule-grid-slot        — wraps one time slot
           .schedule-grid-row       — plenary row (optional)
           .schedule-grid-row       — per-track parallel row (optional)
         .schedule-grid-slot        — next time slot, separated from
                                      the one above by a single hairline

     Cards inside .schedule-grid-cell / .schedule-grid-plenary render
     with the same markup and CSS as in the list view, so they look
     identical across both modes.
  ------------------------------------------------------------ */
  .schedule-grid {
    display: grid;
    /* No border-top and no margin-block-start here: the first slot sits
       flush under the day heading, matching the list view which also has
       no rule above its first row. Previously the grid's container
       border-top + 0.5rem margin produced the stray horizontal rule
       above "08:00 Registration" and the small vertical offset visible
       only in grid view — both gone once this unit lines up with the
       list view's top edge. */
  }

  .schedule-grid-slot {
    /* Match the list view's row padding so a grid slot and a list row have
       the same vertical rhythm. The two views are meant to be visually
       indistinguishable at every level — card, row, divider spacing. */
    padding-block: 0.5rem;
  }
  .schedule-grid-slot + .schedule-grid-slot {
    border-top: var(--schedule-hairline);
  }

  .schedule-grid-row {
    display: grid;
    /* minmax(0, 1fr) — without the 0 minimum, a long unbreakable word in
       one cell would push that column out and squeeze its neighbours to
       a single character wide. */
    grid-template-columns:
      var(--schedule-grid-time-col)
      repeat(var(--schedule-cols), minmax(0, 1fr));
    /* Top-aligned — matches list view's align-items: start, and reads
       better with parallel cards of different heights (different
       description lengths) since track chips and titles line up along a
       single top baseline.
       One gap value applies to rows and columns — 1rem, matching the
       list view so the first card in either view sits at the same x.
       Extra breathing room between parallel track cards is added as a
       margin on cards after the first (see .schedule-grid-cell +
       .schedule-grid-cell below) — that way only the column-to-column
       junction widens; the time-to-first-card gap stays in sync with
       list view. */
    align-items: start;
    gap: var(--schedule-gap);
  }

  /* Extra horizontal breathing room between parallel track cards only.
     Applied as a left margin on every cell after the first, so the
     grid-row's `gap: 1rem` still governs the time→first-card distance
     (matches list view, no layout shift between views) while adjacent
     parallel cells get an effective 1.5rem of separation. */
  .schedule-grid-cell + .schedule-grid-cell {
    margin-inline-start: 0.5rem;
  }

  /* When a slot contains both a plenary row AND a per-column row (two
     .schedule-grid-row children), drop the gap between them — they belong
     to the same time slot and shouldn't read as two separated rules. */
  .schedule-grid-row + .schedule-grid-row {
    margin-block-start: 0.25rem;
  }

  .schedule-grid-time {
    font-size: var(--font-size-small);
    color: var(--schedule-ink-muted);
    font-variant-numeric: tabular-nums;
  }

  .schedule-grid-cell { min-height: var(--schedule-cell-min-height); }

  .schedule-grid-plenary {
    /* Span the full width of the grid (time column stays in column 1). */
    grid-column: 2 / span var(--schedule-cols);
  }

  /* Title-as-link styling — when a session has a primary speaker
     URL, only the TITLE is wrapped in <a>, never the whole card.
     This avoids the nested-anchor issue (description markdown links
     would otherwise force the browser to silently split the outer
     <a>, fragmenting the card). The title link inherits the title's
     colour + weight by default; underline only appears on hover/
     focus so the schedule reads as titles + metadata, not a sea of
     blue links. */
  .schedule-title-link {
    color: inherit;
    text-decoration: none;
  }
  .schedule-title-link:hover,
  .schedule-title-link:focus-visible {
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
  }
  .schedule-title-link:focus-visible {
    outline: 2px solid var(--schedule-link);
    outline-offset: 2px;
    border-radius: var(--schedule-radius);
  }

  /* ------------------------------------------------------------
     Compact mode

     Toggled by the Compact checkbox in the toolbar (JS flips the
     [data-compact] attribute on .schedule). Hides the long description
     block and tightens vertical rhythm so the whole schedule reads as
     a dense overview — good for scanning, for printing, and for when
     you already know which sessions you care about.

     Everything else stays identical: same cards, same speaker bylines,
     same track chips. Uncheck to get the abstracts back.
  ------------------------------------------------------------ */
  .schedule[data-compact="true"] .schedule-desc {
    display: none;
  }
  .schedule[data-compact="true"] .schedule-session {
    padding-block: 0.2rem;
  }
  .schedule[data-compact="true"] .schedule-row-list,
  .schedule[data-compact="true"] .schedule-grid-slot {
    /* Both views get the same tightened row padding in compact mode.
       Without the grid-slot half of this selector, grid view keeps the
       default 0.5rem block padding while list view drops to 0.35rem,
       which showed up as extra vertical whitespace in view-by-track. */
    padding-block: 0.35rem;
  }
  .schedule[data-compact="true"] .schedule-grid-row + .schedule-grid-row {
    /* Plenary→per-col margin inside a shared slot also tightens, matching
       the overall compact rhythm. */
    margin-block-start: 0.15rem;
  }
  /* Narrow-screen compact override.
     At <=900px the grid collapses to a stacked layout where every cell
     (.schedule-grid-cell / .schedule-grid-plenary / .schedule-grid-time)
     picks up `padding-block: 0.5rem` to space the stack visually — see
     the @media block below. The slot's own padding is zeroed at narrow
     (cells handle their own spacing), so the compact override needs to
     match list view's `.schedule-row-list` compact padding of 0.35rem
     exactly — per-cell padding IS the row's block padding here. */
  @media (max-width: 900px) {
    .schedule[data-compact="true"] .schedule-grid-row > * {
      padding-block: 0.35rem;
    }
  }

  /* ------------------------------------------------------------
     View Transition — crossfade when toggling list ↔ grid

     The JS wraps the [data-view] swap in document.startViewTransition()
     on supporting browsers (Chromium, Safari 18+). The default root
     snapshot crossfades between the two views at 180ms, which reads as
     a soft dissolve — fast enough not to feel sluggish, slow enough to
     communicate that content changed. Non-supporting browsers get an
     instant swap. `prefers-reduced-motion` shortens it to a flash.
  ------------------------------------------------------------ */
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 180ms;
    animation-timing-function: ease;
  }
  @media (prefers-reduced-motion: reduce) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
      animation-duration: 1ms;
    }
  }

  /* ------------------------------------------------------------
     Footer line
  ------------------------------------------------------------ */
  .schedule-footer {
    margin-block-start: 2rem;
    padding-block-start: 1rem;
    border-top: var(--schedule-hairline);
    font-size: var(--font-size-small);
    color: var(--schedule-ink-muted);
  }

  /* Secondary "for agents" footer — appears immediately after the
     primary footer, sharing the same muted style. No top border (the
     primary footer's rule already separates the block from the
     schedule). Tighter top margin since it's part of the same footer
     cluster, not a new section. `code` elements get a faint background
     so the MCP URL reads as a literal endpoint, not body text. */
  .schedule-footer--agents {
    margin-block-start: 0.4rem;
    padding-block-start: 0;
    border-top: none;
    font-size: var(--font-size-x-small, 0.8rem);
  }
  .schedule-footer--agents code {
    background: var(--schedule-panel);
    padding: 0.05rem 0.3rem;
    border-radius: var(--schedule-radius);
    font-size: 0.95em;
  }

  /* ------------------------------------------------------------
     Narrow screens

     Below ~900px the parallel-track grid gets unreadable, so we
     collapse each row to: time label on the left, all sessions for
     that time stacked vertically on the right. Track chips inside
     each card make it obvious which track a session belongs to.
  ------------------------------------------------------------ */
  @media (max-width: 900px) {
    .schedule-row-list {
      grid-template-columns: var(--schedule-time-col-narrow) 1fr;
      gap: 0.6rem;
    }

    .schedule-grid-row {
      /* Drop back to a simple two-column row: time on the left, a stacked
         column of cells on the right. Match the list view's narrow gap
         (0.6rem) so the time column and first card sit at the same x
         coordinate in both views — otherwise grid would keep the 1rem
         default gap and every card in grid view would be nudged 0.4rem
         further right than its list-view counterpart. */
      grid-template-columns: var(--schedule-time-col-narrow) 1fr;
      gap: 0.6rem;
    }

    /* At narrow widths the parallel-column breathing room is moot —
       cells stack vertically — so drop the cell margin that adds
       horizontal separation between parallel cards at wide widths.
       Without this, stacked cells would sit 0.5rem right of the first
       cell, which ragged-indents the list. */
    .schedule-grid-cell + .schedule-grid-cell {
      margin-inline-start: 0;
    }

    /* Zero the slot's own padding at narrow — at wide widths the slot
       IS the row (containing one or two grid-rows whose children flow
       horizontally), so the slot's padding is the row padding. At
       narrow widths, every child of .schedule-grid-row stacks vertically
       in the right-hand column with its own `padding-block: 0.5rem`,
       so the slot's padding would stack on top and produce double the
       vertical rhythm of list view. Pushing slot padding to 0 here lets
       the per-cell padding do the work — matching the list view's
       single `padding-block: 0.5rem` per row for single-session slots,
       and correctly accumulating for multi-session slots. */
    .schedule-grid-slot {
      padding-block: 0;
    }

    .schedule-grid-row > * {
      /* Restore default stacking flow inside the right-hand column so
         multiple cells/plenaries land underneath each other rather than
         spilling onto a second grid row. */
      grid-column: 2;
      padding-inline: 0;
      padding-block: 0.5rem;
    }

    .schedule-grid-row > .schedule-grid-time {
      grid-column: 1;
      grid-row: 1 / -1;
    }

    .schedule-grid-cell:empty { display: none; }
    .schedule-grid-cell + .schedule-grid-cell {
      border-top: 1px dashed var(--schedule-rule);
    }

    .schedule-grid-plenary {
      grid-column: 2;
    }
  }

}
