/* 🌐 The published calendar — the one page of the board that lives on
   somebody else's website, inside an <iframe>, read by students on phones.

   It is a member of the same family as the board (Algorithmic Botany, the
   calm Operate dialect): the times and codes are set in JuliaMono, the
   season is the theme — 🍂 Fall is ice, 🌸 Spring is sakura. But a block's
   COLOUR here is its CATEGORY, not its course (Alperen, 2026-09-08:
   "mandatory, fac. elective and uni. elective should all have very distinct
   colors") — a student reads "must I be there?" off the fill, and the code
   printed on the block says which course. Four fills, far apart in hue, all
   carrying white text ≥ 4.5:1; the board's ten course tones are not used.
   It is PAPER by default, not the dark plate: it sits inside a
   faculty page that is almost certainly light, and a near-black rectangle
   framed in white reads as a broken embed. `?theme=dark` gives the plate.

   One signature: the NOW line — a thin rule in the season's colour across
   today's column at the current minute, with a dot on the rail. A public
   timetable is looked at to answer "what is on now, and where", and the line
   answers it before the eye has read a single code. Everything else stays
   quiet: hairlines, one accent, no decoration that is not information.

   There is no drag here and nothing to press but the chips: a change to the
   live board reaches this page only when Publish is pressed again. */

@font-face {
  font-family: "JuliaMono";
  font-style: normal; font-weight: 400; font-display: swap;
  src: url(fonts/juliamono-regular.woff2) format("woff2");
}
@font-face {
  font-family: "JuliaMono";
  font-style: normal; font-weight: 500; font-display: swap;
  src: url(fonts/juliamono-medium.woff2) format("woff2");
}
@font-face {
  font-family: "STIX Two Text";
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url(fonts/stix-two-normal-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2191, U+2193, U+2212;
}
@font-face {
  font-family: "STIX Two Text";
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url(fonts/stix-two-normal-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* 📄 paper — cold, like the board's ice, never cream */
  --bg: #f3f5f8;
  --panel: #ffffff;
  --cell: #e9edf2;
  --line: #d5dbe3;
  --ink: #171b22;
  --muted: #5b6470;
  --accent: #2f5aa8;          /* 🍂 ice, deepened for paper */
  --accent-ink: #ffffff;
  --bad: #b8302f;             /* the one verdict ink: a clash, a day too many */
  /* 🔔 the "there is news" blue. NOT --accent: that is the SEASON's ink and
     turns sakura pink in Spring, and the ask was a blue glow. */
  --fresh: #2f5aa8;
  --row: 58px;                /* one hour — 58 since the type grew (v0.68) */
  --gap: 3px;                 /* the ten-minute break */
  --pitch: calc(var(--row) + var(--gap));
  --mono: "JuliaMono", ui-monospace, "Cascadia Mono", Consolas, monospace;
  --serif: "STIX Two Text", "Iowan Old Style", Charter, Georgia, serif;
  --sans: "Segoe UI", Arial, sans-serif;
  color-scheme: light;
}
body.spring { --accent: #b6407c; }        /* 🌸 sakura, deepened for paper */
body[data-theme="dark"] {
  --bg: #0e0f11; --panel: #16181c; --cell: #1e2128; --line: #262a30;
  --ink: #e9eaec; --muted: #a9aeb6; --accent: #5b93e0; --accent-ink: #0f1013;
  /* the paper red is a 2.2:1 whisper on this plate — the refusals it spells
     out (a clashing section, one COOP day too many) are the loudest thing the
     picker ever says, so it lightens with everything else */
  --bad: #ff8b84; --fresh: #6fa8ff;
  color-scheme: dark;
}
/* ⚠️ html carries a background of its own, so IT — not body — paints the
   canvas behind the page: with the dark variables declared on body alone the
   overscroll strip stayed paper (measured in Chrome). embed_theme.js stamps
   the same word on <html> before the first paint, and this reads it. */
/* ⚠️ color-scheme belongs HERE as well as on body: it is :root that the
   browser asks about the VIEWPORT scrollbar and the overscroll strip, and
   `:root { color-scheme: light }` left a white scrollbar down the side of
   the dark page (the body rule governs the page's own scrollers only). */
html[data-theme="dark"] { background: #0e0f11; color-scheme: dark; }
body[data-theme="dark"].spring {
  --bg: #100e12; --panel: #1a171c; --cell: #221d26; --line: #2c2630;
  --accent: #e78bb8;
}

/* the category fills — the block's colour IS its category. The four are the
   board's own category inks (catpill), deepened one step so white text holds
   on both stops; "unclassified" is a grey nobody mistakes for the others. */
.cat-mand { --ta: #b8302f; --tb: #8e1f23; }  /* mandatory           */
.cat-dept { --ta: #2f63b8; --tb: #214a91; }  /* department elective */
.cat-fac  { --ta: #1f8a4c; --tb: #156838; }  /* faculty elective    */
.cat-uni  { --ta: #7b4fcf; --tb: #5b37a6; }  /* university elective */
.cat-none { --ta: #6b7280; --tb: #4b5563; }  /* unclassified        */
/* Out — an hour another faculty holds, whatever its category: pink, a hue
   none of the four above comes near (embed.js paints the per-course shade) */
.out      { --ta: #c4418b; --tb: #9c2d6c; }

html { background: var(--bg); }
body.embed {
  font-family: var(--sans); font-size: .85rem; line-height: 1.45;
  color: var(--ink); background: var(--bg);
  min-height: 100vh; display: flex; flex-direction: column;
}
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
button:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
[hidden] { display: none !important; }

/* ---------------------------------------------------------------- the bar */
.bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px;
  padding: 10px 14px; background: var(--panel); border-bottom: 1px solid var(--line);
}
.seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 4px;
       background: var(--cell); }
.seg button {
  padding: 5px 11px; border-radius: 3px; font-size: .8rem; font-weight: 600;
  color: var(--muted); white-space: nowrap;
  transition: background-color .12s, color .12s;
}
.seg button:hover { color: var(--ink); }
.seg button.on { background: var(--accent); color: var(--accent-ink); }
/* ⚠️ .4 made the one disabled chip this page has — 🌸 Spring on a board
   with no spring term — a 1.73:1 ghost on paper and 2.31:1 in the dark
   (measured in Chrome 2026-09-17). The strike is what says "not now", so the
   ink does not have to whisper it as well — .6 was still only 2.39:1 on
   paper, .75 clears 3:1 in both. */
.seg button:disabled { opacity: .75; text-decoration: line-through;
                       cursor: not-allowed; }
.caption { font-family: var(--serif); font-weight: 600; font-size: 1.05rem;
           letter-spacing: .01em; }
.caption .t { color: var(--accent); }
#seg-day { display: none; }
/* 👤 my week: the chip is lit like any other; ✎ beside it opens the picker */
.seg.my button:last-child { padding: 5px 8px; }
body.my #seg-dept button, body.my #seg-year button { opacity: .45; }
.dl { margin-left: auto; font-size: .78rem; font-weight: 600; color: var(--ink);
      background: var(--cell); border-radius: 3px; padding: 6px 11px; }
.dl:hover { background: var(--line); }
.dl:disabled { opacity: .5; cursor: wait; }
/* 🔔 the change log: a chip like ⬇ PNG, with the count of what is new riding
   on its corner — absolute, so a badge that appears can never move the bar */
.bell { position: relative; font-size: .78rem; font-weight: 600; color: var(--ink);
        background: var(--cell); border-radius: 3px; padding: 6px 11px; }
.bell:hover { background: var(--line); }
.bell .n { position: absolute; top: -7px; right: -7px; min-width: 18px; padding: 0 4px;
           border-radius: 9px; background: var(--accent); color: var(--accent-ink);
           font-family: var(--mono); font-size: .68rem; line-height: 18px;
           text-align: center; }
/* 🔔 no number, a GLOW (Alperen, 2026-09-16: "new updates should lose their
   numbers after user sees them if possible, if not, just make the button glow
   blue if new thing happened in 72 hours"). Where storage round-trips, the
   badge is the answer and opening the panel clears it; inside the faculty's
   cross-site frame nothing round-trips — the badge sat at 4 for weeks, a
   decoration nobody could clear — so there the bell glows for 72 hours
   instead, and stops as soon as the panel has been opened. */
.bell.fresh { color: var(--fresh); animation: bellfresh 2.4s ease-in-out infinite;
              box-shadow: 0 0 0 2px var(--fresh), 0 0 6px 0 var(--fresh); }
@keyframes bellfresh {
  0%, 100% { box-shadow: 0 0 0 2px var(--fresh), 0 0 4px 0 var(--fresh); }
  50%      { box-shadow: 0 0 0 2px var(--fresh), 0 0 12px 2px var(--fresh); }
}
/* 🌓 the theme switch: the same chip as ⬇ PNG and the 🔔 it stands beside */
.chip { font-size: .92rem; line-height: 1; color: var(--ink); background: var(--cell);
        border-radius: 3px; padding: 6px 9px; }
.chip:hover { background: var(--line); }

/* ---------------------------------------------------------------- picker */
.sheet { position: fixed; inset: 0; z-index: 20; background: rgba(15, 18, 24, .45);
         display: flex; align-items: flex-start; justify-content: center; padding: 24px 12px;
         overflow: auto; }
.panel { width: min(720px, 100%); background: var(--panel); color: var(--ink);
         border: 1px solid var(--line); border-radius: 4px;
         box-shadow: 0 18px 50px rgba(0, 0, 0, .3); display: flex; flex-direction: column;
         max-height: calc(100vh - 48px); }
.phead { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
         padding: 14px 18px 10px; border-bottom: 1px solid var(--line); }
.phead h2 { font-family: var(--serif); font-weight: 600; font-size: 1.1rem; }
.pcount { color: var(--muted); font-size: .78rem; margin-left: auto; }
.pcoop { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
         padding: 8px 18px; border-bottom: 1px solid var(--line); background: var(--cell); }
.pcoop .l { font-size: .78rem; font-weight: 600; }
.pcoop.pdept { border-bottom: 0; padding-bottom: 2px; }
.pcoop .seg { background: var(--panel); }
.pcoop .seg button { padding: 3px 10px; font-size: .76rem; font-family: var(--mono); }
/* the "lectures on Mon, Tue — 2 of 2 days" line is ALWAYS its own line under
   the chips, whatever the panel's width — a wrapping flex row put it beside
   the chips on a wide panel and under them on a narrow one */
.pcoop .days { flex-basis: 100%; font-size: .74rem; color: var(--muted); min-height: 1.2em; }
.pcoop .days.over { color: var(--bad); font-weight: 600; }
.plist { overflow: auto; padding: 6px 18px 12px; }
.plist h3 { font-size: .72rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
            color: var(--muted); margin: 14px 0 4px; }
.prow { border-bottom: 1px solid var(--line); padding: 7px 0; }
.prow label { display: grid; grid-template-columns: auto auto auto 1fr; gap: 0 8px;
              align-items: center; cursor: pointer; }
.prow input { width: 16px; height: 16px; accent-color: var(--accent); }
.prow .code { font-family: var(--mono); font-weight: 500; font-size: .84rem; }
.prow .tag { font-style: normal; font-size: .6rem; font-weight: 600; letter-spacing: .06em;
             text-transform: uppercase; color: #fff; padding: 1px 5px; border-radius: 2px;
             background: linear-gradient(90deg, var(--ta), var(--tb)); white-space: nowrap; }
.prow .tag.out { background: linear-gradient(90deg, #c4418b, var(--ta)); }
.prow .nm { color: var(--muted); font-size: .8rem; overflow: hidden; text-overflow: ellipsis;
            white-space: nowrap; }
.prow.on .nm { color: var(--ink); }
.pchoice { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 6px 0 2px 24px; }
.pchoice .l { font-size: .72rem; color: var(--muted); min-width: 62px; }
.pchoice .seg { flex-wrap: wrap; }
.pchoice .seg button { padding: 3px 8px; font-size: .74rem; font-family: var(--mono); }
.pchoice .none { font-size: .72rem; color: var(--muted); font-style: italic; }
/* ⛔ an option that overlaps a chosen lecture: red, struck, dead — the one
   verdict ink this page has, spent on the one thing it refuses */
/* ⚠️ no fade at all: the strike, the verdict ink and not-allowed already say
   refused, and every step of dimming took the one sentence the picker exists
   to say further out of reach — .55 read 3.0:1 on paper, .85 still 4.08:1
   (measured 2026-09-17). ⚠️ The `opacity: 1` is load-bearing and not a
   default: the chip IS disabled, so without it `.seg button:disabled` fades
   it to .75 and the red drops to 3.46:1 on paper. The live chips beside it
   are --muted, so the red is the LOUD one here, which is right. */
.pchoice .seg button.bad { color: var(--bad); text-decoration: line-through;
                           opacity: 1; background: none; cursor: not-allowed; }
.pchoice .seg button.bad.on { background: #b8302f; color: #fff; opacity: 1;
                              text-decoration: none; cursor: pointer; }
.prow.bad label { cursor: not-allowed; }
/* ⚠️ dim the BOX, never the words: at .5 the code of a refused row read
   below 4.5:1 in both themes. The ⛔ .why line under it is already in the
   verdict ink, and the checkbox is disabled — between them the row says it
   cannot be taken without any text having to fade out of legibility. */
.prow.bad .code, .prow.bad .nm { color: var(--muted); }
.prow .why { grid-column: 2 / -1; font-size: .72rem; color: var(--bad); }
.prow input:disabled { opacity: .4; }
.pfoot { display: flex; gap: 8px; justify-content: flex-end; padding: 10px 18px 14px;
         border-top: 1px solid var(--line); }
.pfoot button { font-size: .8rem; font-weight: 600; padding: 7px 14px; border-radius: 3px; }
.pfoot .ghost { color: var(--muted); background: var(--cell); }
.pfoot .primary { color: var(--accent-ink); background: var(--accent); }
.pfoot button:hover { filter: brightness(1.08); }
body.picking { overflow: hidden; }

/* --------------------------------------------------------------- 🔔 changes
   One shape for both surfaces: the panel behind the bell and the standalone
   /embed/<token>/changes page a website frames on its own. The sentences are
   the board's own (publish.py writes them, the owner edits them), so they are
   set in the mono of the times and codes they are about; everything this page
   adds around them — the stamp, the term, the cohorts — stays muted. */
.chg { padding: 10px 0; border-bottom: 1px solid var(--line); }
.chg:last-child { border-bottom: 0; }
/* ⚠️ every property .plist h3 sets is spelled out again here: the panel's
   entries live INSIDE .plist, and the picker's group heading is muted, spaced
   and UPPERCASE — which shouted an owner's own typed title on the calendar
   while the standalone page (no .plist) printed it as typed. One heading, one
   look, on both surfaces. */
.chg h3 { font-family: var(--serif); font-weight: 600; font-size: 1rem;
          color: var(--ink); letter-spacing: .01em; text-transform: none;
          margin: 0; display: flex; align-items: baseline; gap: 10px;
          flex-wrap: wrap; }
.chg h3 .when { font-family: var(--mono); font-size: .72rem; font-weight: 400;
                color: var(--muted); }
.chg p.note { white-space: pre-line; margin: 4px 0 2px; }
.chg h4 { margin-top: 8px; font-size: .7rem; font-weight: 600; letter-spacing: .06em;
          text-transform: uppercase; color: var(--muted); }
.chg ul { list-style: none; margin-top: 5px; display: grid; gap: 4px; }
.chg li { display: grid; grid-template-columns: 18px 1fr; gap: 0 6px;
          align-items: baseline; }
.chg li .ico { text-align: center; color: var(--muted); font-size: .8rem; }
.chg li .txt { font-family: var(--mono); font-size: .8rem; }
.chg li .who { font-family: var(--sans); color: var(--muted); font-size: .72rem; }
.chg .more { color: var(--muted); font-size: .74rem; margin-top: 4px; }
.chgnone { color: var(--muted); font-style: italic; padding: 10px 0; }
/* the standalone page is a column of text, not a week: it is read, not scanned */
body.changes #main { max-width: 46rem; }

/* ---------------------------------------------------------------- the week */
#main { padding: 12px 14px 8px; flex: 1; }
.week {
  display: grid;
  grid-template-columns: 58px repeat(5, minmax(0, 1fr));
  grid-template-rows: 28px repeat(10, var(--row));
  gap: var(--gap);
}
.dh, .hr {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: .72rem; font-weight: 500;
  color: var(--muted); background: var(--panel); border-radius: 2px;
}
.dh { position: relative; }
.dh .short { display: none; }
/* today: the season's ink under the day, the same rule the NOW line draws */
.dh.today { color: var(--ink); }
.dh.today::after { content: ""; position: absolute; left: 10px; right: 10px;
                   bottom: 0; height: 2px; background: var(--accent); }
.hr { flex-direction: column; line-height: 1.15; font-size: .66rem; }
/* the end of the hour, under its start. ⚠️ Written as an opacity it read
   3.12:1 on the rail; --muted is the page's own quiet ink and is chosen to
   hold, so it is said in colour instead (2026-09-17). */
.hr small { color: var(--muted); font-size: .58rem; }
.cell { background: var(--cell); border-radius: 2px; }
/* one overlay per day, exactly ten rows tall — the blocks are placed in it by
   hour, which is robust inside an iframe where measured pixels drift */
.col { position: relative; grid-row: 2 / span 10; pointer-events: none;
       height: calc(var(--pitch) * 10 - var(--gap)); }
.col > * { pointer-events: auto; }

/* One step up from v0.65's sizes (Alperen, 2026-09-08: "Embedded Block fonts
   should be a little bit bigger to easy to see") — the page is read on a
   faculty website from across a corridor, not on the desk the board is edited
   at. The hour grew with them (--row 54 → 58), so a 1-hour block still holds
   its code line and its room. Two things do NOT step, and each says why where
   it lives: the category tag, and a 3-way stack's code. */
.blk {
  position: absolute; border-radius: 3px; color: #fff; overflow: hidden;
  padding: 5px 7px; font-size: .82rem; line-height: 1.2;
  display: flex; flex-direction: column; gap: 1px;
  background: linear-gradient(135deg, var(--ta), var(--tb));
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
  transition: filter .12s;
}
.blk:hover { filter: brightness(1.07); z-index: 3; }
.blk .code { font-family: var(--mono); font-weight: 500; font-size: .8rem;
             display: flex; align-items: center; gap: 4px; min-width: 0; }
.blk .code .cc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* the tag is the one thing that does NOT grow: it shares the line with the
   CODE, and every point it takes is a point the code loses — measured, a
   .64rem tag ellipsised "CHEM 1030 §L2" on a 1000px board and cost three
   narrow codes their last digit at 1500px */
.blk .code i { font-style: normal; flex: none; font-family: var(--sans); font-size: .58rem;
               font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
               padding: 0 4px; border-radius: 2px; line-height: 1.5; }
/* the category tag: MAN / FAC.E / DEPT.E / UNI.E — a light plate so it reads
   on any of the fills; "lab" stays the dark one */
.blk .code .tag { background: rgba(255, 255, 255, .22); }
.blk .code .ects { background: rgba(255, 255, 255, .22); letter-spacing: 0;
                   font-variant-numeric: tabular-nums; }
.blk .code .kind { background: rgba(8, 9, 12, .35); font-weight: 400; }
/* Out: pink on the left, the category on the right — left to right, not the
   135° the in-house blocks use */
.blk.out { background: linear-gradient(90deg, var(--ta), var(--tb)); }
.blk .nm { font-size: .75rem; overflow: hidden; display: -webkit-box;
           -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
/* 🏫 the room: the thing a student is looking for — its own line, bold, big,
   on a light plate so it reads at arm's length (Alperen, 2026-09-08) */
.blk .room { margin-top: auto; align-self: flex-end; font-family: var(--mono);
             font-weight: 700; font-size: .95rem; letter-spacing: .01em;
             color: #fff; background: rgba(255, 255, 255, .18);
             padding: 1px 6px; border-radius: 2px; max-width: 100%;
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.blk.h1 { padding: 3px 6px; }
.blk.h1 .nm { display: none; }
.blk.h1 .room { font-size: .86rem; padding: 0 5px; }
.blk.h2 .nm { -webkit-line-clamp: 1; }
/* the 3-way stack is the one place that does not take the step: at an 88px
   column the .8rem code ellipsised "AGTE 3391" to "AGTE 339…" and the .8rem
   room ate the "5" of "Y (-1B05)" — measured on the live Fall board at
   900-1500px. A truncated course code is worse than a small one, so here only
   the NAME and the room grow, and the room by a hair. */
.blk.narrow { padding: 3px 5px; }
.blk.narrow .code { font-size: .64rem; }
.blk.narrow .nm { font-size: .64rem; }
.blk.narrow .room { font-size: .76rem; }
.blk.narrow.h1 .nm { display: none; }
/* § several sections share this hour — a dashed inner edge says "one of" */
.blk.sec { outline: 1px dashed rgba(255, 255, 255, .55); outline-offset: -4px; }
.blk.sec .secs { font-family: var(--mono); font-size: .7rem; opacity: .95;
                 white-space: normal; line-height: 1.25; }
/* …but the list of sections wraps, so in a stack it stays where it was: at
   .7rem SOFL's fourteen tags spilled 30px out of a 1280px-wide board that
   held them all before */
.blk.narrow.sec .secs { font-size: .62rem; }
/* a section this cohort's own lecture sits on is struck, not dropped: the hour
   is real, the student just cannot attend it (v0.72) */
/* ⚠️ .55 read 3.78:1 in the phone's day list on paper; the LINE is what
   says "not this one", so the tag itself stays readable (2026-09-17) */
.secs s { text-decoration: line-through; text-decoration-thickness: 1.5px; opacity: .75; }

/* ---------------------------------------------- 🌐 online, beside the week
   Lectures taught online or somewhere this faculty does not timetable: they
   have no day and no hour, so they are a column of cards next to Friday
   rather than blocks (Alperen, 2026-09-08). The column exists only while the
   cohort has one — without `has-online` this is the five-day week it was. */
.week.has-online {
  grid-template-columns: 58px repeat(5, minmax(0, 1fr)) minmax(120px, .75fr);
}
.dh.onl { font-family: var(--sans); font-weight: 600; }
.onlcol { display: flex; flex-direction: column; gap: var(--gap);
          height: calc(var(--pitch) * 10 - var(--gap)); overflow: auto; }
.oblk {
  flex: none; border-radius: 3px; color: #fff; overflow: hidden;
  padding: 5px 7px; font-size: .82rem; line-height: 1.2;
  display: flex; flex-direction: column; gap: 1px;
  background: linear-gradient(135deg, var(--ta), var(--tb));
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
}
.oblk .code { font-family: var(--mono); font-weight: 500; font-size: .8rem;
              display: flex; align-items: center; gap: 4px; min-width: 0; }
.oblk .code .cc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.oblk .code i { font-style: normal; flex: none; font-family: var(--sans); font-size: .58rem;
                font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
                padding: 0 4px; border-radius: 2px; line-height: 1.5;
                background: rgba(255, 255, 255, .22); }
.oblk .nm { font-size: .75rem; overflow: hidden; display: -webkit-box;
            -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
.oblk .meta { margin-top: 2px; font-family: var(--mono); font-size: .7rem; opacity: .95; }
/* 🌐 the online look: turquoise → white (the stops come from colorsOf, like
   every plate) with DARK ink, and a dark translucent tag instead of the light
   one — the light plate would swallow white text */
.oblk.online, .drow.orow.online { color: #0b3a37; }
.oblk.online .code i, .drow.orow.online .code .tag {
  background: rgba(11, 58, 55, .16); color: #0b3a37; border: 1px solid rgba(11, 58, 55, .3); }
.oblk.online .meta { opacity: .85; }
/* the phone's list: the same cards under a heading of their own, on every day
   — an online lecture belongs to no single one */
.dayonl { font-size: .72rem; font-weight: 600; letter-spacing: .06em;
          text-transform: uppercase; color: var(--muted); margin: 14px 0 2px; }
.drow.orow .when { font-family: var(--sans); font-size: .75rem; }

/* ⏱ NOW — the signature: one rule, the season's ink, at the current minute */
.now { position: absolute; left: -2px; right: -2px; height: 2px; z-index: 4;
       background: var(--accent); pointer-events: none; }
.now::before { content: ""; position: absolute; left: -5px; top: -3px;
               width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }

.empty { margin: 26px auto; max-width: 34rem; text-align: center;
         color: var(--muted); font-size: .9rem; }

/* ---------------------------------------------------------------- one day
   The phone view: the same lectures as a list down the day. */
.daylist { display: none; }
.drow { display: grid; grid-template-columns: 52px 4px 1fr; gap: 0 10px;
        padding: 8px 0; border-bottom: 1px solid var(--line); align-items: stretch; }
.drow .when { font-family: var(--mono); font-size: .77rem; color: var(--muted);
              line-height: 1.3; }
.drow .when b { display: block; color: var(--ink); font-weight: 500; }
.drow { grid-template-columns: 52px 6px 1fr; }
.drow .stripe { border-radius: 2px; background: linear-gradient(180deg, var(--ta), var(--tb)); }
.drow .code { font-family: var(--mono); font-weight: 500; font-size: .88rem; }
.drow .code i { font-style: normal; color: var(--muted); font-weight: 400; margin-left: 4px; }
.drow .code .tag { font-family: var(--sans); font-size: .68rem; font-weight: 600;
                   letter-spacing: .05em; color: #fff; padding: 0 4px; border-radius: 2px;
                   background: linear-gradient(90deg, var(--ta), var(--tb)); }
.drow .code .ects { font-family: var(--sans); font-size: .73rem; font-weight: 600;
                    color: var(--muted); }
.drow .nm { font-size: .92rem; }
.drow .room { font-family: var(--mono); font-weight: 700; font-size: 1rem;
              color: var(--ink); margin-top: 2px; }
.drow.sec .secs { font-family: var(--mono); font-size: .82rem; color: var(--ink); }

/* ---------------------------------------------------------------- legend */
.legend { margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--line); }
.legend ul { list-style: none; display: grid; gap: 4px 18px;
             grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.legend li { display: grid; grid-template-columns: 14px auto 1fr auto; gap: 0 8px;
             align-items: baseline; font-size: .78rem; padding: 2px 0; }
.legend .sw { width: 12px; height: 12px; border-radius: 2px; align-self: center;
              background: linear-gradient(135deg, var(--ta), var(--tb)); }
.legend .code { font-family: var(--mono); font-weight: 500; font-size: .74rem; }
.legend .nm { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend .ects { grid-column: 4; color: var(--muted); font-size: .72rem;
                white-space: nowrap; font-variant-numeric: tabular-nums; }
.legend .who { color: var(--muted); grid-column: 3; font-size: .72rem;
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-key { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 4px 16px;
              font-size: .72rem; color: var(--muted); }
.key::before { content: ""; display: inline-block; width: 9px; height: 9px;
               border-radius: 2px; margin-right: 6px; vertical-align: -1px; }
.key.mand::before { background: #b8302f; }
.key.dept::before { background: #2f63b8; }
.key.fac::before { background: #1f8a4c; }
.key.uni::before { background: #7b4fcf; }
.key.sec::before { background: none; border: 1px dashed var(--muted); }
.key.out::before { background: linear-gradient(90deg, #c4418b, #6b7280); }
.key.onl::before { background: linear-gradient(90deg, #1fb7ad, #effefc); border: 1px solid #1fb7ad; }
.key.shade::before { background: linear-gradient(90deg, #d0463f, #8e1f23); }

/* ---------------------------------------------------------------- foot */
.foot { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
        padding: 8px 14px 10px; font-size: .7rem; color: var(--muted);
        font-family: var(--mono); }
.foot a { color: var(--muted); text-decoration: none; }
.foot a:hover { color: var(--ink); text-decoration: underline; }

/* ---------------------------------------------------------------- gone */
.gone { margin: auto; max-width: 30rem; padding: 40px 20px; text-align: center; }
.gone-icon { font-size: 2rem; margin-bottom: 8px; }
.gone h1 { font-family: var(--serif); font-weight: 600; font-size: 1.3rem;
           margin-bottom: 8px; }
.gone p { color: var(--muted); }

/* ---------------------------------------------------------------- narrow
   Under 680 px the week cannot hold five readable columns: the page becomes
   one day, chosen with a chip row (today by default). */
@media (max-width: 680px) {
  .week, .legend .who { display: none; }
  .daylist, #seg-day { display: block; }
  #seg-day { display: inline-flex; }
  .bar { padding: 8px 10px; gap: 6px 10px; }
  .dl { margin-left: 0; }
  .prow label { grid-template-columns: auto auto 1fr; }
  /* ⚠️ the tag lands in the 1fr track here, and its gradient plate stretched
     216 px behind the word MAN — a pill is as wide as its word (2026-09-17) */
  .prow .tag { justify-self: start; }
  .prow .nm { grid-column: 2 / -1; }
  .seg button { padding: 5px 9px; font-size: .76rem; }
  #main { padding: 6px 12px 8px; }
  .legend ul { grid-template-columns: 1fr; }
}
@media (max-width: 980px) {
  .dh .full { display: none; }
  .dh .short { display: inline; }
}
@media (prefers-reduced-motion: reduce) {
  .seg button, .blk { transition: none; }
  /* the ring stays — it is the information; only the pulse goes */
  .bell.fresh { animation: none; }
}

/* ↻ a repeat offering: the plan is in the other term, the owning faculty
   opens it this term for the students repeating it — a dashed inner frame,
   so it reads as "not everyone's" before the tag is read */
.blk.repeat { outline: 2px dashed rgba(255, 255, 255, .78); outline-offset: -4px; }
.drow.repeat { outline: 1px dashed var(--muted); outline-offset: -2px; }
.key.rep::before { background: none; border: 1px dashed var(--bad); }
#courses li.repeat .sw { outline: 1px dashed currentColor; outline-offset: 1px; }

/* ---------------------------------------------------------------- 🌓 dark
   The plate, for the reader who asked for it — ?theme=dark, or 🌙 in the bar
   (Alperen, 2026-09-16: "live calendar should have dark mode, png downloads
   should remember light/dark mode and draw it accordingly"). The choice is
   resolved and remembered entirely in the browser: the 🛡 shield Worker keys
   this HTML on its pathname and its language, so a third server-side variant
   would be a copy it could never replay.

   Almost everything follows the variables at the top of this file; what is
   left here is the handful of inks chosen against PAPER. ⚠️ No font-size
   belongs in this block — test_board.py reads the FIRST rule a block selector
   matches to check the type stepped up, and a size here would be it. */
body[data-theme="dark"] .sheet { background: rgba(0, 0, 0, .62); }
body[data-theme="dark"] .panel { box-shadow: 0 18px 50px rgba(0, 0, 0, .55); }
/* the two plates that are LIGHT by design keep their dark ink: a block's fill
   is its category, the same colour in both themes, and 🌐 online is turquoise
   → white wherever it is drawn (see .oblk.online above) */
body[data-theme="dark"] .blk, body[data-theme="dark"] .oblk {
  box-shadow: 0 1px 3px rgba(0, 0, 0, .5); }
body[data-theme="dark"] .dh.today::after { opacity: .9; }
/* 🌐 …but only the CARD is a plate. The phone's day list draws the same
   lecture as a row with a stripe and no fill, so on the dark sheet #0b3a37
   landed straight on --bg and read 1.53:1, its tag 1.46:1 (measured in Chrome
   2026-09-17). The row takes the page's own ink; the tag keeps the turquoise,
   as a wash it can be read through. */
body[data-theme="dark"] .drow.orow.online { color: var(--ink); }
body[data-theme="dark"] .drow.orow.online .code .tag {
  color: var(--ink); background: rgba(31, 183, 173, .22);
  border-color: rgba(31, 183, 173, .5); }
