/* The page layer on top of modernist.css. Modernist owns colour, type, spacing and
   radius (zero, everywhere); this file declares the self-hosted fonts, the one
   documented off-system palette (the lesson chips), and lays the page out.

   The vocabulary is junshengliau.me's document page: 2px ink rules between bands,
   section headings in the pixel face, labels in Silkscreen, prose in Archivo,
   everything flush left - button labels included. */

/* — Fonts —————————————————————————————————————————————————————————————— */

/* Latin subsets, served from this origin. Archivo is one variable file covering
   400-800, which is every weight used here. Licences are in fonts/OFL-*.txt. */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/archivo-latin-var.woff2") format("woff2");
}

@font-face {
  font-family: "Press Start 2P";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/press-start-2p-latin-400.woff2") format("woff2");
}

@font-face {
  font-family: "Silkscreen";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/silkscreen-latin-400.woff2") format("woff2");
}

/* — Tokens ————————————————————————————————————————————————————————————— */

:root {
  /* Modernist ships light only, so this page does too. */
  color-scheme: light;

  /* The two pixel faces, layered on Modernist the way the main site does. */
  --font-pixel: "Press Start 2P", "Courier New", monospace;
  --font-screen: "Silkscreen", "Courier New", monospace;

  /* Lesson chips. The one documented off-system exception: Modernist is a mono
     red-on-white system with no categorical palette, and eight lesson types need
     eight colours that read apart at a glance. All eight are pale tints at one
     OKLCH lightness, so no chip outweighs another, and every one carries
     --color-text at well over 4.5:1 (measured: see README). Hues follow the
     original plan's, so returning readers find the same lessons in the same
     colours. Activity is the neutral one on purpose: it is also a link, and its
     border and arrow are what mark it out. */
  --lesson-1: oklch(0.92 0.045 250);   /* English 1: Fin Lit - blue */
  --lesson-2: oklch(0.91 0.05 10);     /* English 2: Family Feud - rose */
  --lesson-3: oklch(0.92 0.06 150);    /* English 3: Resume - green */
  --lesson-4: oklch(0.94 0.075 95);    /* English 4: Pitch - yellow */
  --lesson-5: oklch(0.91 0.05 300);    /* English 5: AI - violet */
  --lesson-act: oklch(0.92 0.008 60);  /* Activity (English) - warm grey */
  --lesson-cn: oklch(0.91 0.055 50);   /* Chinese - peach */
  --lesson-sp: oklch(0.92 0.05 205);   /* Sports Day - teal */

  /* Page shell */
  --page-max: 1120px;
  --page-pad: clamp(16px, 3.5vw, 40px);
  --rule: 2px solid var(--color-text);
}

/* — Page shell ————————————————————————————————————————————————————————— */

html {
  /* A tap flashes the page's own ink, not the platform's blue. */
  -webkit-tap-highlight-color: color-mix(in srgb, var(--color-text) 12%, transparent);
  /* The sticky bars would otherwise cover whatever a focus or a jump scrolls to. */
  scroll-padding-top: calc(env(safe-area-inset-top, 0px) + 120px);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  font-size: 16px;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

[hidden] {
  display: none !important;
}

.container {
  max-width: var(--page-max);
  margin: 0 auto;
  padding-left: max(var(--page-pad), env(safe-area-inset-left, 0px));
  padding-right: max(var(--page-pad), env(safe-area-inset-right, 0px));
}

/* Everything that holds text may shrink below its content's width; long words wrap
   rather than pushing the page sideways at 320. */
main {
  overflow-wrap: break-word;
}

button,
select,
input,
textarea {
  font: inherit;
  color: inherit;
}

/* Modernist's buttons, at this page's touch size. Flush left, always. */
.btn {
  min-height: 44px;
  justify-content: flex-start;
  text-align: left;
  font-family: var(--font-screen);
  font-weight: 400;
  font-size: 14px;
  padding-inline: 14px;
}

.btn-secondary {
  border: var(--rule);
  background: var(--color-neutral-100);
}

/* — Cursors ———————————————————————————————————————————————————————————————
   Everything that opens, toggles or chooses shows the hand, children included, so
   no inner span falls back to the arrow or the text I-beam. Disabled controls say
   so with not-allowed; text fields keep the I-beam. Body copy is untouched, so text
   selection there works as before. (A native <select>'s option list is the
   browser's own UI and keeps its own cursor.) */
summary,
summary *,
select,
button,
button *,
[role="button"],
[role="button"] *,
a[href],
a[href] *,
label[for],
label:has(> input[type="checkbox"]),
label:has(> input[type="checkbox"]) * {
  cursor: pointer;
}

button:disabled,
button:disabled *,
select:disabled,
[aria-disabled="true"] {
  cursor: not-allowed;
}

input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]),
textarea {
  cursor: text;
}

/* Move up / Move down in the edit dialogs: drawn arrows, for the same reason as
   Back below (U+2191 and U+2193 are in none of the faces). The buttons carry the
   words in aria-label. */
.arrow {
  display: block;
  width: 10px;
  height: 14px;
}

.arrow-up {
  background:
    linear-gradient(currentColor 0 0) left 4px top 0 / 2px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 2px top 2px / 6px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 0 top 4px / 10px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 4px top 6px / 2px 8px no-repeat;
}

.arrow-down {
  background:
    linear-gradient(currentColor 0 0) left 4px bottom 0 / 2px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 2px bottom 2px / 6px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 0 bottom 4px / 10px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 4px bottom 6px / 2px 8px no-repeat;
}

/* "Back" carries a drawn arrow: none of the self-hosted faces has U+2190, so a typed
   one fell back to another font, at another size. Same 2px steps as the others. */
.btn-back::before {
  content: "";
  flex: none;
  width: 8px;
  height: 14px;
  margin-right: 4px;
  background:
    linear-gradient(currentColor 0 0) left 0 top 6px / 2px 2px no-repeat,
    linear-gradient(currentColor 0 0) left 2px top 4px / 2px 6px no-repeat,
    linear-gradient(currentColor 0 0) left 4px top 2px / 2px 10px no-repeat,
    linear-gradient(currentColor 0 0) left 6px top 0 / 2px 14px no-repeat;
}

/* — Hero ——————————————————————————————————————————————————————————————— */

.hero {
  border-bottom: var(--rule);
  padding-top: env(safe-area-inset-top, 0px);
}

.hero-inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  padding-block: 44px 36px;
}

/* The CV hero's headline: the pixel face. Its floor is set by the longest word -
   "Maewangwittayakom" is 17 cells, and at 16px that fits the 288px a 320 screen
   leaves inside its gutters. */
.title {
  font-family: var(--font-pixel);
  font-weight: 400;
  font-size: clamp(16px, 3.8vw, 36px);
  line-height: 1.35;
  letter-spacing: -0.5px;
  margin: 0;
  max-width: 24ch;
  overflow-wrap: anywhere;
}

.hero-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  align-self: stretch;
  min-width: 0;
}

/* The project's logo, drawn as SVG (lettering is DynaPuff, OFL, converted to paths,
   so nothing depends on a font loading). Two lockups, one visible at a time, behind
   one accessible name. Phones get the two-line lockup above the title, full column
   width; its height is also held to 36% of a short (landscape) screen. */
.hero-logo {
  flex: none;
  width: min(100%, 520px, calc(36dvh * 459.5 / 221.9));
  aspect-ratio: 459.5 / 221.9;
  background: url("logo-nokhook-phone.svg") 0 0 / 100% 100% no-repeat;
  border-radius: 0;
}

/* From here the title's longest word (17 cells at 36px, 604px) and a 300px logo fit
   side by side, so the stacked lockup fills the column beside the title, intro and
   buttons: its top on the title's, its bottom on the buttons', its right edge on
   the page's. Below it, the column would be too narrow to read, so it stacks. */
@media (min-width: 1024px) {
  .hero-inner {
    flex-direction: row;
    align-items: stretch;
    gap: 40px;
  }

  .hero-text {
    flex: 0 1 660px;
    min-width: 610px;
  }

  .hero-logo {
    order: 1;
    flex: 1 0 300px;
    width: auto;
    aspect-ratio: auto;
    background-image: url("logo-nokhook-stack.svg");
  }
}

.hero-body {
  font-size: 17px;
  line-height: 1.6;
  max-width: 68ch;
  color: var(--color-neutral-800);
  text-wrap: pretty;
  margin: 0;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.status {
  margin: 0;
  font-family: var(--font-screen);
  font-size: 13px;
  color: var(--color-accent-700);
}

.status:empty {
  display: none;
}

/* — Sections ——————————————————————————————————————————————————————————— */

/* One column on a phone; a heading column and a content column once there is
   room. The /jobxy page's section, to the pixel. */
.section {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  padding-block: 28px;
}

.section + .section {
  border-top: var(--rule);
}

@media (min-width: 760px) {
  .section {
    grid-template-columns: 200px minmax(0, 1fr);
    gap: 40px;
    padding-block: 36px;
  }
}

.heading {
  font-family: var(--font-pixel);
  font-weight: 400;
  font-size: 10px;
  line-height: 1.6;
  letter-spacing: 0;
  color: var(--color-accent-700);
  margin: 0;
}

h3 {
  font-size: 17px;
  margin: 0 0 6px;
}

/* — Check before using ————————————————————————————————————————————————— */

/* The one warning on the page, so it gets the accent: a tint and an accent rule. */
.flag {
  background: var(--color-accent-100);
  border: 2px solid var(--color-accent-700);
  padding: 14px 16px;
  max-width: 76ch;
}

/* Block, not grid: a grid item will not shrink below its longest word, so one long
   pasted string would widen the page. */
.flag ul {
  margin: 0;
  padding-left: 20px;
  line-height: 1.55;
}

.flag li + li,
details.std li + li {
  margin-top: 8px;
}

/* — The key ———————————————————————————————————————————————————————————— */

.key {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 24px;
}

.key > div {
  min-width: 0;
}

.key dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  margin: 0;
}

.key dt {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.key dd {
  margin: 0;
}

.eg {
  font-family: var(--font-screen);
  font-size: 18px;
  margin: 0 0 8px;
}

/* — Filters ———————————————————————————————————————————————————————————— */

.filters {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--color-bg);
  border-top: var(--rule);
  border-bottom: var(--rule);
  padding-top: env(safe-area-inset-top, 0px);
}

.filters-inner {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px 10px;
  align-items: end;
  padding-block: 10px;
}

@media (min-width: 760px) {
  .filters-inner {
    grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
    gap: 12px;
  }
}

.field {
  display: grid;
  gap: 3px;
  font-family: var(--font-screen);
  font-size: 12px;
  line-height: 1.2;
  color: var(--color-neutral-700);
  min-width: 0;
}

.filters-end {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 12px;
}

@media (min-width: 760px) {
  .filters-end {
    grid-column: auto;
  }
}

.count {
  font-family: var(--font-screen);
  font-size: 13px;
  color: var(--color-neutral-700);
  white-space: nowrap;
}

/* Phones: four labelled selects and Show all are five rows, about 210px of sticky
   bar - a third of an iPhone SE. So below the four-column layout the bar folds to
   one row, a Filters button beside the count, and opens in place on a tap. */
.filters-toggle {
  display: none;
}

@media (max-width: 759px) {
  .filters-toggle {
    display: inline-flex;
    grid-column: 1;
    grid-row: 1;
    justify-self: start;
    align-self: center;
  }

  .filters-toggle {
    white-space: nowrap;
  }

  /* How many filters are on, as a solid ink badge, so the folded bar says the
     timetable is filtered without the label growing past half the row. */
  .filters-on {
    min-width: 20px;
    margin-left: 2px;
    padding: 0 5px;
    font-size: 13px;
    line-height: 20px;
    text-align: center;
    background: var(--color-text);
    color: var(--color-bg);
  }

  .filters-toggle::after {
    content: "";
    flex: none;
    width: 10px;
    height: 16px;
    margin-left: 4px;
    background:
      linear-gradient(currentColor 0 0) right 0 top 5px / 10px 2px no-repeat,
      linear-gradient(currentColor 0 0) right 2px top 7px / 6px 2px no-repeat,
      linear-gradient(currentColor 0 0) right 4px top 9px / 2px 2px no-repeat;
  }

  .filters.open .filters-toggle::after {
    background:
      linear-gradient(currentColor 0 0) right 4px top 5px / 2px 2px no-repeat,
      linear-gradient(currentColor 0 0) right 2px top 7px / 6px 2px no-repeat,
      linear-gradient(currentColor 0 0) right 0 top 9px / 10px 2px no-repeat;
  }

  .filters-end {
    display: contents;
  }

  .filters .count {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    white-space: normal;
  }

  .filters #clear {
    grid-column: 1 / -1;
    justify-self: start;
  }

  .filters:not(.open) .field,
  .filters:not(.open) #clear {
    display: none;
  }
}

/* A select without the platform's rounded chrome. The arrow is drawn in CSS so it
   takes the ink token rather than a colour baked into an image. 16px text, or iOS
   zooms the page on focus and never zooms back. */
.select {
  position: relative;
  display: block;
  min-width: 0;
}

.select select,
.input {
  appearance: none;
  width: 100%;
  min-height: 44px;
  padding: 8px 10px;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.3;
  color: var(--color-text);
  background: var(--color-neutral-100);
  border: var(--rule);
  border-radius: 0;
}

.select select {
  padding-right: 32px;
  text-overflow: ellipsis;
}

.select::after {
  content: "";
  position: absolute;
  right: 12px;
  top: 50%;
  margin-top: -3px;
  border: 6px solid transparent;
  border-top-color: var(--color-text);
  border-bottom: 0;
  pointer-events: none;
}

/* Desktop Chromium: the customizable select, so the open list is the page's own -
   options a 44px row each, flush left, the hand cursor on hover, the accent tint
   for the one under the pointer or the keyboard, an ink bar for the chosen one.
   Progressive enhancement only, and fenced off three ways: the browser has to
   support base-select; it must not be WebKit (Safari 26 also supports it, and
   hanging-punctuation is WebKit-only, so this keeps Safari on its native list);
   and there has to be a mouse, so every phone keeps the OS picker. The closed box
   keeps the rules above - the 2px rule, 16px Archivo, 44px - and .select::after's
   arrow, so the platform's own picker icon is hidden. Keyboard, type-ahead and
   change events are the browser's own in both modes. */
@media (hover: hover) and (pointer: fine) {
  @supports (appearance: base-select) and (not (hanging-punctuation: first)) {
    /* .select select and .input set appearance: none, so match their weight. */
    select,
    .select select,
    select.input,
    ::picker(select) {
      appearance: base-select;
    }

    select {
      border-radius: 0;
    }

    select::picker-icon {
      display: none;
    }

    ::picker(select) {
      margin-block: 4px;
      padding: 0;
      max-height: min(360px, 50dvh);
      overflow-y: auto;
      border: var(--rule);
      border-radius: 0;
      background: var(--color-neutral-100);
      color: var(--color-text);
      box-shadow: none;
      font-family: var(--font-body);
      font-size: 16px;
    }

    option {
      display: flex;
      align-items: center;
      justify-content: flex-start;
      gap: 0;
      min-height: 44px;
      padding: 8px 12px 8px 14px;
      border-radius: 0;
      text-align: left;
      cursor: pointer;
    }

    option::checkmark {
      display: none;
    }

    option:checked {
      font-weight: 700;
      background: var(--color-neutral-100);
      box-shadow: inset 4px 0 0 var(--color-text);
    }

    option:hover,
    option:focus-visible {
      background: var(--color-accent-200);
      color: var(--color-accent-700);
      outline: none;
    }
  }
}

/* Modernist's .input:hover greys the border; here the 2px ink rule holds. */
.select select:hover,
.input:hover {
  background: var(--color-bg);
  border-color: var(--color-text);
}

.select select:focus-visible,
.input:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* — Day tables ————————————————————————————————————————————————————————— */

#days {
  display: grid;
  gap: 32px;
  padding-block: 28px 8px;
}

/* Grid items default to min-width: auto, which lets a wide table stretch its day
   and the page with it. Zero, so the table scrolls inside .wrap instead. */
#days > *,
.section > *,
.cards > *,
.lib-body > * {
  min-width: 0;
}

.day h2 {
  font-size: 22px;
  margin: 0 0 10px;
}

.day-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  margin-bottom: 10px;
}

.day-head h2 {
  margin: 0;
}

/* Wide tables scroll inside their own frame, never the page. The frame is a
   focusable region so a keyboard can scroll it too. */
/* position: relative contains the edit column header, which is visually hidden by
   absolute positioning: without it the label escapes the scroll frame and widens
   the page. */
.wrap {
  position: relative;
  overflow-x: auto;
  border: var(--rule);
  background: var(--color-neutral-100);
}

.wrap:focus-visible {
  outline-offset: 2px;
}

/* The frame app.js puts round every .wrap. When the table is wider than its frame,
   a hint line appears above it and the side with more columns is shaded, so it is
   plain that the table moves sideways. The shading sits in the table's own grid
   cell and ignores the pointer, so swipes still reach the table. */
.wrap-frame {
  display: grid;
  grid-template-areas: "hint" "table";
  grid-template-columns: minmax(0, 1fr);
}

.wrap-frame > .wrap {
  grid-area: table;
}

.scroll-hint {
  grid-area: hint;
  display: none;
  margin: 0 0 6px;
  font-family: var(--font-screen);
  font-size: 13px;
  line-height: 16px;
  color: var(--color-neutral-700);
}

.wrap-frame.scrolls > .scroll-hint {
  display: block;
}

.scroll-hint::before {
  content: "Scroll for more columns";
  display: inline-block;
  padding-right: 14px;
  background:
    linear-gradient(currentColor 0 0) right 4px top 3px / 2px 10px no-repeat,
    linear-gradient(currentColor 0 0) right 2px top 5px / 2px 6px no-repeat,
    linear-gradient(currentColor 0 0) right 0 top 7px / 2px 2px no-repeat;
}

.wrap-frame::before,
.wrap-frame::after {
  content: "";
  grid-area: table;
  z-index: 1;
  width: 28px;
  margin-block: 2px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
}

.wrap-frame::before {
  justify-self: start;
  margin-left: 2px;
  background: linear-gradient(to left, transparent, color-mix(in srgb, var(--color-text) 22%, transparent));
}

.wrap-frame::after {
  justify-self: end;
  margin-right: 2px;
  background: linear-gradient(to right, transparent, color-mix(in srgb, var(--color-text) 22%, transparent));
}

.wrap-frame.more-left::before,
.wrap-frame.more-right::after {
  opacity: 1;
}

.table {
  font-variant-numeric: tabular-nums;
}

.table th,
.table td {
  padding: 8px 12px;
  white-space: nowrap;
  vertical-align: middle;
  text-align: left;
}

.table th {
  color: var(--color-neutral-700);
}

.table tbody tr:last-child td {
  border-bottom: 0;
}

/* Members is the one column allowed to wrap, so a five-person team does not make
   every table scroll. */
.table td.members {
  white-space: normal;
  min-width: 12rem;
}

td.per {
  font-weight: 700;
}

td.per small {
  display: block;
  font-weight: 400;
  font-size: 12px;
  color: var(--color-neutral-700);
}

tr.ev td {
  background: var(--color-neutral-200);
}

tr.ev td.evt {
  font-weight: 700;
  white-space: normal;
}

td.tot {
  white-space: normal;
  color: var(--color-neutral-700);
  border-top: var(--rule);
  border-bottom: 0;
}

td.tot b {
  color: var(--color-text);
}

.empty {
  margin: 0;
  font-family: var(--font-screen);
  font-size: 14px;
}

/* — Lesson chips ——————————————————————————————————————————————————————— */

.chip {
  display: inline-block;
  padding: 2px 8px;
  font-size: 14px;
  line-height: 1.4;
  color: var(--color-text);
  white-space: nowrap;
}

.chip-1 { background: var(--lesson-1); }
.chip-2 { background: var(--lesson-2); }
.chip-3 { background: var(--lesson-3); }
.chip-4 { background: var(--lesson-4); }
.chip-5 { background: var(--lesson-5); }
.chip-act { background: var(--lesson-act); }
.chip-cn { background: var(--lesson-cn); }
.chip-sp { background: var(--lesson-sp); }

/* Activity (English) is a link into the library: a 44px target with the ink rule,
   and the site's link hover - the accent-200 tint with the text stepping down to
   accent-700, which is what keeps it above 4.5:1 on the tint. */
a.chip-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 12px;
  border: var(--rule);
  text-decoration: underline;
  text-underline-offset: 3px;
}

a.chip-link::after {
  content: "\00a0\2192";
}

a.chip-link:hover {
  background: var(--color-accent-200);
  color: var(--color-accent-700);
}

/* — Periods per person ————————————————————————————————————————————————— */

.load {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 24px;
}

.load > div {
  min-width: 0;
}

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

.load li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  border-top: 1px solid var(--color-divider);
  padding: 5px 0;
}

.load li span {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.load-section {
  border-top: var(--rule);
  margin-top: 28px;
}

/* — Activity Library ——————————————————————————————————————————————————— */

#lib {
  padding-bottom: 56px;
}

.libbar {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--color-bg);
  border-bottom: var(--rule);
  padding-top: env(safe-area-inset-top, 0px);
}

/* On a phone: Back and Edit share the first row and the tabs are one row that
   scrolls inside itself, so the sticky bar stays two controls tall rather than
   wrapping to four. */
.libbar-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "back edit" "tabs tabs";
  align-items: center;
  gap: 8px 12px;
  padding-block: 10px;
}

.libbar-inner > a {
  grid-area: back;
  justify-self: start;
}

.libbar-inner > .edit-toggle {
  grid-area: edit;
}

.tabs {
  grid-area: tabs;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  min-width: 0;
}

.tab {
  flex: none;
}

/* One row from 600px: a phone on its side is 640-930 wide but only 340-430 tall,
   and the two-row bar would take a third of that. */
@media (min-width: 600px) {
  .libbar-inner {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "back tabs edit";
    gap: 8px 16px;
  }
}

.tab {
  min-height: 44px;
  padding: 0 14px;
  font-family: var(--font-screen);
  font-size: 14px;
  text-align: left;
  border: var(--rule);
  background: var(--color-neutral-100);
  color: var(--color-text);
  cursor: pointer;
}

.tab:hover {
  background: var(--color-bg);
}

.tab[aria-pressed="true"] {
  background: var(--color-text);
  color: var(--color-bg);
}

.lib-body {
  display: grid;
  gap: 28px;
  padding-top: 32px;
}

.lib-body > * {
  min-width: 0;
}

.lib-head {
  display: grid;
  gap: 14px;
  justify-items: start;
}

.lib-head .hero-body {
  margin: 0;
}

/* Opened from a timetable chip: the period it was opened for. */
.ctx {
  border: var(--rule);
  border-left-width: 8px;
  background: var(--color-neutral-100);
  padding: 14px 16px;
  line-height: 1.6;
  max-width: 76ch;
}

.ctx-kicker {
  display: block;
  font-family: var(--font-screen);
  font-size: 13px;
  color: var(--color-accent-700);
  margin-bottom: 4px;
}

details.std {
  border: var(--rule);
  background: var(--color-neutral-100);
}

/* list-style and the webkit marker go because the label below replaces them; Safari
   would otherwise draw its own triangle beside it. */
details.std > summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  min-height: 44px;
  padding: 8px 14px;
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}

details.std > summary::-webkit-details-marker {
  display: none;
}

details.std > summary:hover {
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
}

/* The same label the cards carry, so every box that opens says so the same way.
   The "/ ''" keeps it out of the summary's accessible name: details already
   announces expanded and collapsed. */
details.std > summary::after {
  content: "Open";
  content: "Open" / "";
}

details.std[open] > summary::after {
  content: "Close";
  content: "Close" / "";
}

details.std > :not(summary) {
  margin: 0 14px 14px;
}

details.std ul {
  padding-left: 20px;
}

.lvh h2 {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  font-size: 22px;
  margin: 0 0 6px;
}

.lvh h2 .chip {
  font-family: var(--font-screen);
  font-weight: 400;
  font-size: 14px;
  letter-spacing: 0;
}

.lvh p {
  margin: 0;
  color: var(--color-neutral-700);
  max-width: 76ch;
}

.cards,
.wu {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: 12px;
  margin-top: 14px;
}

.act-card,
.wu > div {
  border: 2px solid var(--color-divider);
  background: var(--color-neutral-100);
  min-width: 0;
}

.act-card {
  scroll-margin-top: 140px;
}

.act-card.open {
  grid-column: 1 / -1;
  border-color: var(--color-text);
}

.wu > div {
  padding: 14px;
}

.wu h3 {
  margin: 0 0 2px;
}

.wu p {
  margin: 6px 0 0;
}

.ch {
  display: grid;
  gap: 5px;
  width: 100%;
  min-height: 44px;
  padding: 14px;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  line-height: 1.45;
}

.ch:hover {
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
}

.ch:focus-visible {
  outline-offset: -4px;
}

/* Flex, so a Top pick badge that wraps starts its line flush left. */
.nm {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 17px;
  line-height: 1.25;
}

.tg,
.mt {
  color: var(--color-neutral-700);
  font-size: 14px;
}

.more {
  font-family: var(--font-screen);
  font-size: 13px;
  color: var(--color-accent-700);
}

.more::before {
  content: "Open guide";
}

.act-card.open .more::before {
  content: "Close guide";
}

/* The open/close label, on the cards and on every summary. None of the three
   self-hosted faces has U+25BE or U+25B4 (measured in the browser), so a typed arrow
   fell back to another font per character. The arrow is drawn instead: three rows
   on a 2px grid, the pixel faces' own step, in the label's colour. */
.more::before,
details.std > summary::after {
  display: inline-block;
  padding-right: 18px;
  font-family: var(--font-screen);
  font-weight: 400;
  font-size: 13px;
  line-height: 16px;
  color: var(--color-accent-700);
  background:
    linear-gradient(currentColor 0 0) right 0 top 5px / 10px 2px no-repeat,
    linear-gradient(currentColor 0 0) right 2px top 7px / 6px 2px no-repeat,
    linear-gradient(currentColor 0 0) right 4px top 9px / 2px 2px no-repeat;
}

.act-card.open .more::before,
details.std[open] > summary::after {
  background:
    linear-gradient(currentColor 0 0) right 4px top 5px / 2px 2px no-repeat,
    linear-gradient(currentColor 0 0) right 2px top 7px / 6px 2px no-repeat,
    linear-gradient(currentColor 0 0) right 0 top 9px / 10px 2px no-repeat;
}

/* Modernist's accent tag, for the one badge on the card. */
.star {
  padding: 1px 7px;
  font-family: var(--font-screen);
  font-weight: 400;
  font-size: 11px;
  background: var(--color-accent-100);
  color: var(--color-accent-800);
}

.pn {
  border-top: 2px solid var(--color-divider);
  padding: 6px 14px 18px;
  display: grid;
  gap: 10px;
  line-height: 1.6;
}

.pn > section {
  min-width: 0;
}

.pn h3 {
  margin: 12px 0 6px;
}

.pn h4 {
  font-size: 15px;
  margin: 12px 0 4px;
}

.pn ul {
  margin: 0;
  padding-left: 20px;
}

.pn p {
  margin: 0 0 8px;
  max-width: 76ch;
}

.pn .table td,
.pn .table th,
details.std .table td {
  white-space: normal;
}

.pn .table td:first-child,
details.std .table td:first-child {
  white-space: nowrap;
  font-weight: 700;
}

.tip {
  background: var(--color-neutral-200);
  padding: 8px 12px;
}

/* — Footer ————————————————————————————————————————————————————————————— */

/* Under both views: the same baseline line junshengliau.me closes its footer with.
   The rule is the content width, like that site's baseline strip, so it lines up
   with every other rule on the page. The bottom safe area is body's padding
   (above), which sits under this, so it is not added again here; in edit mode
   body's padding also clears the fixed edit bar, so the line is never under it. */
.site-foot-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  border-top: var(--rule);
  padding-block: 20px 28px;
}

.site-foot-note {
  margin: 0;
  font-family: var(--font-screen);
  font-size: 12px;
  color: var(--color-neutral-700);
}

/* — Editing ———————————————————————————————————————————————————————————— */

/* Everything below this line only ever renders after the Edit toggle. A viewer
   who never presses it sees the toggle and nothing else. */

.edit-toggle[aria-pressed="true"] {
  background: var(--color-text);
  color: var(--color-bg);
}

.edit-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding-top: 16px;
}

.row-actions {
  display: flex;
  gap: 6px;
}

.btn-small {
  min-height: 44px;
  min-width: 44px;
  padding-inline: 10px;
  font-size: 13px;
}

.btn-danger {
  border-color: var(--color-accent-700);
  color: var(--color-accent-700);
}

.btn-danger:hover {
  background: var(--color-accent-200);
}

.card-edit {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 0 14px 14px;
}

/* Room under the page for the fixed edit bar. app.js measures the bar into
   --editbar-h (it is three rows tall on a small phone, more than the old fixed
   150px, which left the last controls stuck underneath it). */
body.editing {
  padding-bottom: calc(var(--editbar-h, 150px) + 16px + env(safe-area-inset-bottom, 0px));
}

.editbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  background: var(--color-text);
  color: var(--color-bg);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.editbar-inner {
  display: grid;
  gap: 6px 16px;
  grid-template-columns: minmax(0, 1fr);
  padding-block: 10px;
}

@media (min-width: 900px) {
  .editbar-inner {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }
  .editbar-msg {
    grid-row: 2;
  }
  .editbar-actions {
    grid-column: 2;
    grid-row: 1 / span 2;
  }
}

.editbar-state {
  margin: 0;
  font-family: var(--font-screen);
  font-size: 14px;
}

.editbar-state.dirty::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 8px;
  background: var(--color-accent-500);
}

.editbar-msg {
  margin: 0;
  font-size: 14px;
  line-height: 1.4;
}

.editbar-msg:empty {
  display: none;
}

.editbar-msg.error {
  color: var(--color-accent-300);
}

.editbar-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.editbar .btn-secondary {
  background: transparent;
  color: var(--color-bg);
  border-color: var(--color-bg);
}

.editbar .btn-secondary:hover {
  background: color-mix(in srgb, var(--color-bg) 14%, transparent);
}

.editbar .btn:focus-visible {
  outline-color: var(--color-accent-400);
}

/* — Dialogs ———————————————————————————————————————————————————————————— */

/* The lightbox's frame: a 4px ink border and a black title bar. */
/* Plain CSS, with nothing read from script: the browser's own modal placement
   (fixed, inset 0, margin auto, height fit-content) centres it at its content's
   height, and max-height caps it to the screen less the notch and home indicator,
   so the title bar and the Apply/Cancel footer stay on screen and only the body
   scrolls. vh comes first for a browser without svh; svh is the screen with the
   toolbars showing, so the cap never runs under them. Viewport units are only ever
   a max-height here, never a height: a wrong one can make the dialog shorter, not
   take it to nothing. */
.dlg {
  width: min(760px, calc(100vw - 24px - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px)));
  max-height: calc(100vh - 24px);
  max-height: calc(100svh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  padding: 0;
  border: 4px solid var(--color-text);
  border-radius: 0;
  background: var(--color-bg);
  color: var(--color-text);
  box-shadow: var(--shadow-lg);
}

/* Every flex basis below is auto, never 0: in a column whose height is its
   content, WebKit on iPhone resolved flex: 1 (a 0% basis) as a real 0, so the form
   and its body came out 0 tall and only the dialog's 4px borders were drawn - a
   dark bar across the middle of the screen. */
.dlg[open] {
  display: flex;
  flex-direction: column;
}

/* The safety net app.js switches to when a dialog still comes out collapsed: pinned
   12px inside the screen, so its height comes from the screen and not from its
   content, and the whole dialog scrolls. */
.dlg.dlg-safe[open] {
  display: block;
  inset: 12px;
  margin: 0 auto;
  height: auto;
  max-height: none;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.dlg.dlg-safe .dlg-body {
  overflow-y: visible;
}

.dlg::backdrop {
  background: color-mix(in srgb, var(--color-neutral-900) 60%, transparent);
}

.dlg form {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 0 1 auto;
}

.dlg-bar,
.dlg-foot {
  flex: none;
}

.dlg-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 6px 6px 14px;
  background: var(--color-text);
  color: var(--color-bg);
}

.dlg-bar h2 {
  margin: 0;
  font-family: var(--font-pixel);
  font-weight: 400;
  font-size: 11px;
  line-height: 1.5;
  letter-spacing: 0;
}

/* The title takes focus from script on a touch screen (app.js), only so the
   keyboard stays down; it is not a control, so it draws no focus ring. */
.dlg-bar h2:focus {
  outline: none;
}

.dlg-close {
  min-height: 44px;
  min-width: 44px;
  padding: 0 12px;
  border: 0;
  background: var(--color-accent);
  color: var(--color-bg);
  font-family: var(--font-screen);
  font-size: 14px;
  cursor: pointer;
}

.dlg-close:hover {
  background: var(--color-accent-600);
}

/* position: relative keeps the visually hidden labels (position: absolute) inside
   the scrolling body. Without it their containing block was the dialog, they hung
   below it, and the dialog itself became a second, invisible scroller: a field
   focused low in the body could scroll the title bar and footer off screen. */
.dlg-body {
  position: relative;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 16px;
  display: grid;
  gap: 16px;
  min-height: 0;
  flex: 1 1 auto;
  line-height: 1.5;
}

.dlg-body > p {
  margin: 0;
}

.dlg-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-top: var(--rule);
}

.dlg-error {
  flex-basis: 100%;
  margin: 0;
  color: var(--color-accent-700);
  font-weight: 700;
}

.dlg-error:empty {
  display: none;
}

.f {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.f > label,
.f > .f-label,
.fs > legend {
  font-family: var(--font-screen);
  font-size: 13px;
  color: var(--color-neutral-800);
  padding: 0;
}

.f-hint {
  font-size: 13px;
  color: var(--color-neutral-700);
  margin: 0;
}

textarea.input {
  min-height: 88px;
  resize: vertical;
}

.check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  cursor: pointer;
}

.check input {
  width: 22px;
  height: 22px;
  margin: 0;
  accent-color: var(--color-accent-700);
}

.f-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 12px;
}

.fs {
  border: 2px solid var(--color-divider);
  margin: 0;
  padding: 12px;
  display: grid;
  gap: 12px;
  min-width: 0;
}

.list {
  display: grid;
  gap: 10px;
}

.list-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
}

.item-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.item-tools .select {
  flex: 1 1 160px;
}

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

.history li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  padding: 8px 0;
  border-top: 1px solid var(--color-divider);
}

.history li:first-child {
  border-top: 0;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* — Phones ———————————————————————————————————————————————————————————————
   Nothing reads under 12px on a phone: the 10-11px labels (section headings,
   table column heads, dialog titles, the Top pick badge) step up to 12. A phone
   on its side is wider than 759px, so touch screens get it too. */
@media (max-width: 759px), (pointer: coarse) {
  .heading,
  .table th,
  .dlg-bar h2,
  .star {
    font-size: 12px;
  }
}

/* With a dialog open on a touch screen the page behind does not scroll, so a swipe
   that runs off the end of a dialog's body goes nowhere rather than moving the
   timetable underneath. The class is set and cleared by app.js's one dialog close
   routine, not matched by html:has(dialog.dlg[open]): with :has() the page only came
   back once the browser re-ran the selector after the dialog was removed, and where
   it did not, the page stayed overflow: hidden and could not be scrolled at all. */
@media (pointer: coarse) {
  html.dlg-open {
    overflow: hidden;
  }
}

/* A very short screen - a phone on its side with the keyboard up - leaves a fixed
   title bar and footer almost no room between them. There the whole dialog scrolls
   instead, so the focused field gets the height and the footer is a swipe away. */
@media (max-height: 340px) {
  .dlg[open] {
    display: block;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .dlg-body {
    overflow-y: visible;
  }
}

/* In edit mode the fixed edit bar covers the bottom of the screen; a focused or
   jumped-to control stops above it rather than behind it. */
html:has(body.editing) {
  scroll-padding-bottom: calc(var(--editbar-h, 150px) + 16px);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}
