/* Massry EMBA Course Experience Survey
   ----------------------------------------------------------------------------
   The palette and type scale are carried over from the review site so that the
   two read as one system. Light is the primary design; dark redefines only the
   tokens, so no colour is ever defined for the first time inside a media query.

   A note on width. A good share of these are completed on a phone, in the room,
   in the opening minutes of the last Saturday. Phone is the design case here,
   not the fallback: touch targets are 44px, the radio rows are full-width and
   tappable across their whole area, and nothing depends on hover.
   ------------------------------------------------------------------------- */

:root {
  /* University at Albany brand. Purple #46166b and gold #eeb211 are the primary
     colours, purple leading. The same values the schedule site uses, so the two
     Massry sites read as one system rather than two.

     Gold is deliberately rare: the note struck once on a page, not a second
     accent competing with purple. Gold text on a light ground fails contrast at
     the brand value, so --gold-ink is the darkened version used wherever gold
     has to be read rather than merely seen. */
  --purple:     #46166b;
  --purple-deep:#33104e;
  --purple-lift:#5d2a86;
  --gold:       #eeb211;
  --gold-soft:  #fdf3d6;
  --gold-ink:   #8a6508;

  --bg:         #ffffff;
  --surface:    #faf9fb;
  --specimen:   #f3f0f7;
  --ink:        #1a1420;
  --ink-soft:   #4a4356;
  --muted:      #6f6880;
  --rule:       #e2ddea;
  --rule-soft:  #f0edf3;
  --accent:     var(--purple);
  --accent-ink: #ffffff;
  --accent-bg:  #f3f0f7;
  --focus:      var(--purple);

  /* A filled control, and the label on it.
     Kept apart from --accent/--accent-ink because the two invert differently.
     On a light ground the accent IS the fill and white is the label. On a dark
     ground the accent has to lighten to stay visible as a line or a word — and
     a lightened accent used as a fill forces a DARK label, which is how every
     button in the console came to be set in purple on lavender. A button label
     is white in both themes; the fill moves to keep it that way. */
  --btn-bg:     var(--purple);
  --btn-ink:    #ffffff;
  --btn-edge:   var(--purple);
  /* A secondary button carries no fill, so its label has to sit on the page.
     Purple on white; near-white on the dark ground, where white is legible. */
  --btn-ghost:  var(--purple);

  /* Failure is not a brand colour. Errors stay red, so "something is wrong"
     never has to be inferred from the same purple used for every button. */
  --alert:      #a3261b;

  --serif: "Source Serif 4", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  --sans:  "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --measure: 34rem;      /* ~68 characters at this body size */
  --shell:   72rem;      /* the admin console, which is a working surface */

  /* The width of a page's content column. The masthead uses the SAME value, so
     the mark and the first line of text share a left edge. They used to differ
     — masthead on the 72rem shell, body on the reading column — which is
     invisible on a phone, where both sit against the 16px gutter, and obvious
     on a wide screen, where the header drifts off to the left on its own. */
  --content: 42rem;

  --step--1: 0.8125rem;
  --step-0:  1.0625rem;
  --step-1:  1.25rem;
  --step-2:  1.5rem;
  --step-3:  1.9375rem;
  --step-4:  2.5rem;

  --touch: 44px;

  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:         #14101a;
    --surface:    #1c1723;
    --specimen:   #241d2e;
    --ink:        #f2eef7;
    --ink-soft:   #cdc5da;
    --muted:      #968ea6;
    --rule:       #352c42;
    --rule-soft:  #271f31;
    /* Brand purple is too dark to carry an accent on a dark ground, so the
       lifted tint is used. The mark and the gold are unchanged. */
    --accent:     #b795d6;
    --accent-ink: #1c1723;
    --accent-bg:  #2a2137;
    --focus:      #b795d6;
    /* Chosen against #14101a rather than picked by eye: 3.3:1 for the fill
       against the page, which clears the 3:1 a control needs to be seen as a
       control, and 5.7:1 for white on the fill, which clears text contrast
       with room. --purple-lift was tried first and reaches only 1.9:1 against
       this background — a button you have to look for. */
    --btn-bg:     #8348c4;
    --btn-ink:    #ffffff;
    --btn-edge:   #8348c4;
    --btn-ghost:  var(--ink);
    --gold-ink:   #eeb211;
    /* The pinned-notice ground. The light value (#fdf3d6) is a cream
       that would carry light text at about 1.1:1 — unreadable rather
       than merely wrong. This is the same hue held at the surface's
       weight, so a pinned notice still reads as gold-marked without
       inverting the text on it. */
    --gold-soft:  #2c2410;
    --alert:      #e8786a;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg:         #14101a;
  --surface:    #1c1723;
  --specimen:   #241d2e;
  --ink:        #f2eef7;
  --ink-soft:   #cdc5da;
  --muted:      #968ea6;
  --rule:       #352c42;
  --rule-soft:  #271f31;
  /* Brand purple is too dark to carry an accent on a dark ground, so the
     lifted tint is used. The mark and the gold are unchanged. */
  --accent:     #b795d6;
  --accent-ink: #1c1723;
  --accent-bg:  #2a2137;
  --focus:      #b795d6;
  --btn-bg:     #8348c4;
  --btn-ink:    #ffffff;
  --btn-edge:   #8348c4;
  --btn-ghost:  var(--ink);
  --gold-ink:   #eeb211;
  --gold-soft:  #2c2410;
  --alert:      #e8786a;
  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { font-family: var(--serif); font-weight: 600; line-height: 1.2; }
h1 { font-size: var(--step-3); margin: 0 0 0.5rem; }
h2 { font-size: var(--step-2); margin: 2.5rem 0 0.75rem; }
h3 { font-size: var(--step-1); margin: 1.75rem 0 0.5rem; }

p { margin: 0 0 1rem; max-width: var(--measure); }

a { color: var(--accent); text-underline-offset: 0.15em; }

:where(a, button, input, select, textarea):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 3px;
}

/* -------------------------------------------------------------- structure */

.shell {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 16px;            /* the phone gutter */
}

.page { max-width: var(--content); margin: 0 auto; padding: 2rem 16px 5rem; }

/* The masthead carries two lines: who this is, then what this is. A single
   line reading "Course Feedback · Admin" said what the page was without ever
   saying whose it was — and the mark had to sit at 24px to match one line of
   small text. Two lines give it room to be seen. */
.masthead {
  background: var(--purple);
  border-bottom: 3px solid var(--gold);
  padding: 1rem 0;
  color: #ffffff;
}
.masthead a { color: #ffffff; }
.masthead .shell {
  display: flex;
  align-items: center;
  gap: 0.95rem;
  max-width: var(--content);   /* aligned with the page beneath it */
}
.masthead-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.masthead-org {
  font-size: var(--step--1);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--gold);
  line-height: 1.3;
}
.masthead-site {
  font-family: var(--serif);
  font-size: var(--step-1);
  line-height: 1.2;
}

/* A working surface rather than a reading column.
   Set on --content, which the masthead reads too, so the header and the cards
   beneath it keep one left and right edge. Widening `.page` alone leaves the
   masthead at the reading measure and the mismatch reads as a fault — which
   is the console's own recorded mistake, at the head of admin.css. */
body[data-width="wide"] { --content: var(--panel); }

/* Sign out, at the right-hand end of the masthead.
   Ordered last so it sits outside the theme control: the theme group carries
   margin-left:auto, which pushes itself and everything after it to the right,
   and the sign-out is written into the markup while the theme control is
   appended by a script — so source order alone would put it on the wrong side. */
.masthead-actions { order: 3; margin: 0; flex: none; }
.masthead-actions button {
  font: inherit;
  font-size: var(--step--1);
  min-height: 32px;
  padding: 0.25rem 0.7rem;
  color: #ffffff;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
}
.masthead-actions button:hover { background: rgba(255, 255, 255, 0.22); }

@media (max-width: 30rem) {
  /* Three things in a row that fits two. The mark goes first — the wordmark
     beside it says the same — and the school line goes second, because on a
     phone it was four lines of uppercase above the name of the site. What is
     left is "Faculty Portal", the theme control and the way out, which is
     what somebody opening this on a Friday evening actually needs. */
  .masthead .minerva { display: none; }
  .masthead-org { display: none; }
  .masthead-site { white-space: nowrap; }
  .masthead .shell { gap: 0.5rem; }
  .masthead-actions button { padding: 0.25rem 0.55rem; }
}

/* The Minerva mark, used unaltered. The University's style guide permits
   scaling and nothing else — never recoloured, cropped or redrawn. Replacing it
   means replacing the file, not editing it. */
.minerva { width: 42px; height: auto; flex: none; display: block; }

.course-head {
  border-left: 3px solid var(--accent);
  padding-left: 1rem;
  margin: 1.5rem 0 2rem;
}
.course-head .code { font-size: var(--step--1); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.course-head .title { font-family: var(--serif); font-size: var(--step-2); }
.course-head .meta { font-size: var(--step--1); color: var(--muted); }

/* The "before you begin" block, set apart so a student can tell the
   instrument's own voice from the program talking about it. */
.preamble {
  background: var(--specimen);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 1.25rem;
  margin: 0 0 2rem;
}
.preamble p:last-child { margin-bottom: 0; }
.preamble .caution { color: var(--ink-soft); font-style: italic; }

/* ------------------------------------------------------------ form pieces */

fieldset { border: 0; margin: 0 0 2.5rem; padding: 0; }
legend { padding: 0; font-family: var(--serif); font-size: var(--step-1); font-weight: 600; }

.question { margin: 0 0 2rem; }
.question > .prompt { display: block; margin-bottom: 0.75rem; max-width: var(--measure); }
.question .n { color: var(--muted); font-variant-numeric: tabular-nums; margin-right: 0.4em; }

/* A whole-row tappable option. The input is visually small but the label is
   the target, so a thumb never has to find a 16px circle. */
.option {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  min-height: var(--touch);
  padding: 0.6rem 0.85rem;
  margin: 0 0 0.4rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.option:has(input:checked) { border-color: var(--accent); background: var(--accent-bg); }
.option input { margin-top: 0.35rem; accent-color: var(--accent); width: 1.05rem; height: 1.05rem; flex: none; }

/* The seven elements. On a phone this stacks: the element text, then its three
   options beneath it. On a wider screen the options sit in a row to the right,
   which is the grid a reader of the paper instrument expects. */
.element { padding: 1rem 0; border-bottom: 1px solid var(--rule-soft); }
.element:last-child { border-bottom: 0; }
.element .text { display: block; margin-bottom: 0.6rem; max-width: var(--measure); }
.element .choices { display: grid; gap: 0.4rem; }

@media (min-width: 46rem) {
  .element { display: grid; grid-template-columns: 1fr 22rem; gap: 1.5rem 1.5rem; align-items: start; }
  .element .text { margin-bottom: 0; }

  /* The box that opens under an element is a third grid item, so without this
     it drops into the narrow left column and arrives as a cramped sliver
     alongside the ratings. It is a full-width question and wants the full
     width of the row. */
  .element .improve-follow { grid-column: 1 / -1; }
}

/* Room to write in. The answer wanted here is a specific change a student is
   asking for, and a box the size of a search field asks for three words. */
.element .improve-follow { margin-top: 1rem; }
.element .improve-follow textarea { min-height: 6.5rem; }
.element .improve-follow .prompt { display: block; margin-bottom: 0.5rem; max-width: var(--measure); }

textarea {
  width: 100%;
  min-height: 7rem;
  padding: 0.75rem;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  resize: vertical;
}

/* Routed follow-ups appear in place rather than on a new screen, so the
   student can see what they chose and what they are being asked about it. */
.follow { margin: 0.75rem 0 0; padding-left: 1rem; border-left: 2px solid var(--rule); }
.follow[hidden] { display: none; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch);
  padding: 0.6rem 1.4rem;
  font: inherit;
  font-weight: 500;
  color: var(--btn-ink);
  background: var(--btn-bg);
  border: 1px solid var(--btn-edge);
  border-radius: 6px;
  cursor: pointer;
}
.btn.secondary { color: var(--btn-ghost); background: transparent; border-color: var(--accent); }
.btn:hover { background: color-mix(in srgb, var(--btn-bg) 86%, white); }
.btn.secondary:hover { background: var(--accent-bg); }
/* Several controls are links rather than buttons — Open Report, Download
   Package — and a link carries an underline by default, which on a button is
   just noise. */
a.btn { text-decoration: none; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.actions {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: 0.75rem;
  padding: 1rem 0 1.25rem;
  background: linear-gradient(to top, var(--bg) 70%, transparent);
}

.note  { font-size: var(--step--1); color: var(--muted); max-width: var(--measure); }
.error { color: var(--alert); font-size: var(--step--1); }

/* A course a student can give feedback on, on the selection screen. */
.course-card {
  display: block;
  padding: 1.25rem;
  margin: 0 0 0.75rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  text-decoration: none;
  color: inherit;
}
.course-card:hover { border-color: var(--accent); }
.course-card .closes { font-size: var(--step--1); color: var(--muted); }
.course-card.done { opacity: 0.6; }

/* Which of the two instruments this card is.
   A cohort can have a midpoint check on one course and the end-of-course
   survey on another open in the same week, and two cards differing only in a
   closing date is how somebody opens the wrong one and finds five questions
   where they expected twenty. It is set in small caps rather than as another
   line of grey text so that it reads as a label and not as more metadata. */
.course-card .instrument {
  margin: 0.5rem 0 0.15rem;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent);
}
.course-card .instrument-mcc { color: var(--gold-ink); }

/* The profile card.
   Gold on its edge, and the only card on this page that is not a survey. A
   student has just been told their answers carry no link back to them; a card
   in the same colour and shape asking for their name and their employer would
   read as that promise being walked back. The mark is the correction.

   Gold is struck once on a page. This is that once — the survey cards carry
   purple, and the midpoint label's gold ink is on a card that is not shown at
   the same time as an open end-of-course round in practice. */
.course-card.profile-card {
  border-left: 3px solid var(--gold);
}
.course-card .instrument-profile { color: var(--gold-ink); }
/* A finished profile dims like a finished survey, but stays a link: it is the
   one card here that is still worth opening after it is done. */
.course-card.profile-card.done { opacity: 0.7; }
.course-card.profile-card.done:hover { opacity: 1; }

/* ------------------------------------------------------------------------
   The faculty portal's navigation.

   Five sections rather than the console's eleven, so there is no drawer and
   no below-tablet select to collapse into — five chips wrap onto two lines on
   a phone and stay legible, which is the whole reason the console needed a
   drawer and this does not.

   Ported from admin.css rather than copied from it in spirit: same measures,
   same tokens, same filled-control treatment for the current tab. The console
   keeps its own copy because it also has the drawer and the group labels,
   which nothing here uses. Two sets of numbers that must agree would be the
   drift this codebase keeps refusing elsewhere — these do not have to agree,
   because they are two different navigations that happen to look alike.

   An <a> rather than a <button>: these are five URLs, not five panels on one
   page. Bookmarkable, back-buttonable, and they work with scripting off.
   ------------------------------------------------------------------------ */

.tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 1.75rem;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--rule);
}
.tab {
  display: inline-flex;
  align-items: center;
  min-height: 38px;
  padding: 0.35rem 0.8rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
  text-decoration: none;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 6px;
  white-space: nowrap;
}
.tab:hover { border-color: var(--accent); }
/* The current tab is a filled control, so it takes the same white-on-purple a
   button does rather than the inverted dark-on-lavender the raw accent gives
   on a dark ground. */
.tab[aria-current="page"] {
  color: var(--btn-ink); background: var(--btn-bg); border-color: var(--btn-edge);
}

/* The way back, one level up.
   Built from the tab above it — same height, same border, same radius — so it
   reads as part of the navigation rather than as a line of body text somebody
   underlined. Quieter than a tab, because it is where you have been rather
   than where you are. The chevron is a pseudo-element, so the label is the
   only thing a screen reader announces. */
.crumb {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 34px;
  margin: 0 0 1.25rem;
  padding: 0.25rem 0.75rem 0.25rem 0.6rem;
  font-size: var(--step--1);
  color: var(--muted);
  text-decoration: none;
  border: 1px solid var(--rule);
  border-radius: 6px;
}
.crumb::before { content: "\2190"; font-size: 1.05em; line-height: 1; }
.crumb:hover { color: var(--accent); border-color: var(--accent); }

/* A row of controls that filters what is below it — the course filter on
   Students. Set apart from the tabs above by weight rather than by a rule,
   because it narrows a section rather than choosing one. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0 0 1.5rem;
}
.filters .tab { font-size: calc(var(--step--1) * 0.96); min-height: 34px; }

/* ------------------------------------------------------------------------
   The faculty portal: a course's meetings, its class, and what has been
   posted about it.

   These live in styles.css rather than in a file of their own because the
   faculty hostname serves a deliberately short allowlist of assets, and a
   third stylesheet on it would be a third thing to keep in step. They are
   built out of the same surface, rule and muted tokens as the cards above,
   so the two sites go on reading as one system.
   ------------------------------------------------------------------------ */

/* A list of dated things: meetings, and the rounds of an instrument.
   A definition list in spirit — a fact on the left, its qualifier after it —
   set as a list because the qualifier is often absent and an empty table
   cell reads as missing data rather than as nothing to say. */
.meetings {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  border-top: 1px solid var(--rule-soft);
}
.meetings li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.75rem;
  align-items: baseline;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.meetings .when { font-variant-numeric: tabular-nums; }
.meetings .mnote { font-size: var(--step--1); color: var(--muted); }
/* A meeting that has happened. Dimmed rather than dropped: an instructor
   checking the room in November still wants to see the course's whole shape,
   and a list that silently shortens as the term runs is disorienting. */
.meetings li.past { color: var(--muted); }

/* The class. One card each, whether or not they have written anything. */
.people { list-style: none; margin: 0; padding: 0; }
.people .person {
  padding: 1rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.people .person:last-child { border-bottom: 0; }
.people .pname { font-family: var(--serif); font-size: var(--step-1); }
.people .prole { font-size: var(--step--1); color: var(--ink-soft); margin-top: 0.1rem; }
.people .bio {
  margin: 0.5rem 0 0;
  max-width: var(--measure);
  font-size: var(--step--1);
}
/* What a student wants out of the course, labelled rather than run together
   with what they have already done. They are different questions and an
   instructor scanning fourteen of these is looking for one of them. */
.people .bio .lbl {
  font-weight: 600;
  color: var(--accent);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  font-size: 0.85em;
  margin-right: 0.35em;
}
.people .person .note { margin: 0.35rem 0 0; font-style: italic; }

/* A course, named on a row that is not itself a course page: a meeting on
   the Schedule tab, a chip under a student's name. Reads as a label and
   behaves as a link, because on Schedule it is the way into the course. */
.course-tag {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--accent);
  text-decoration: none;
  border: 1px solid var(--rule);
  border-radius: 4px;
  white-space: nowrap;
}
a.course-tag:hover { border-color: var(--accent); background: var(--accent-bg); }
/* The same chip where it is not a link. Muted, so a reader does not try. */
.course-tag.plain { color: var(--muted); font-weight: 500; }

/* The next meeting. One mark on the page, and it is the answer to the
   question the tab exists for, so it is the one place gold is struck here. */
.next-mark {
  padding: 0.1rem 0.45rem;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gold-ink);
  border: 1px solid var(--gold);
  border-radius: 4px;
}

/* The class as rows rather than as profiles. Fourteen names fit on a screen
   and fourteen profiles do not, so the row carries what an instructor scans
   for and the rest is one click away. */
.people.rows .person {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.75rem;
  padding: 0.7rem 0;
}
.people.rows .pname { font-size: var(--step-0); text-decoration: none; }
.people.rows .pname:hover { text-decoration: underline; }
.people.rows .prole { margin: 0; }
/* A student who has written nothing is still a row — the absence is the
   useful fact — but it is set apart from a role somebody did write. */
.people.rows .prole.none { font-style: italic; color: var(--muted); }
.people.rows .pcourses { display: flex; gap: 0.3rem; margin-left: auto; }

@media (max-width: 30rem) {
  /* The chips drop under the name rather than being pushed off the line. */
  .people.rows .pcourses { margin-left: 0; flex-basis: 100%; }
}

/* Notices from the Program Office. */
.notices { list-style: none; margin: 0 0 1rem; padding: 0; }
.notices .notice {
  padding: 1rem 1.15rem;
  margin: 0 0 0.75rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule);
  border-radius: 8px;
}
/* Pinned is the one place gold is struck on this page: a single note, not a
   second accent. Everything else here is purple or nothing. */
.notices .notice.pinned { border-left-color: var(--gold); background: var(--gold-soft); }
.notices .ntitle { font-family: var(--serif); font-size: var(--step-1); }
.notices .nbody { margin-top: 0.4rem; max-width: var(--measure); }
.notices .nmeta { margin-top: 0.6rem; font-size: var(--step--1); color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* Print is always light, whatever the screen is set to.
 *
 * This was already wrong before a theme selector existed. `body` was forced to
 * white and black, but every element inside it reads --ink, --muted and
 * --rule, which in dark mode are pale — so a faculty member on a dark-mode
 * machine printing a report got pale grey text on white paper and a report
 * that had to be squinted at. Redefining the tokens fixes the document rather
 * than its outermost element.
 *
 * Paper has no dark mode, and a report is a document that goes into a file. */
@media print {
  /* !important, and it is the right tool here rather than a shortcut.
   *
   * The dark theme is applied by `:root:not([data-theme="light"])` inside a
   * prefers-color-scheme query, which is more specific than a plain `:root`.
   * So a print override written as `:root { --ink: ... }` loses to it, and a
   * reader whose machine is set to dark still prints pale grey on white —
   * which is the bug this block exists to fix, reintroduced by the fix.
   *
   * Out-specifying it by counting selectors would work and would break the
   * next time either selector is touched. Paper has no dark mode: this is a
   * hard override for a different medium, not a skirmish over cascade order,
   * and it should read as one. */
  :root {
    --bg:         #ffffff !important;
    --surface:    #ffffff !important;
    --specimen:   #f3f0f7 !important;
    --ink:        #1a1420 !important;
    --ink-soft:   #4a4356 !important;
    --muted:      #55505f !important;
    --rule:       #cfc8da !important;
    --rule-soft:  #e6e1ec !important;
    --accent:     #46166b !important;
    --accent-ink: #ffffff !important;
    --accent-bg:  #f3f0f7 !important;
    --btn-bg:     #46166b !important;
    --btn-ink:    #ffffff !important;
    --btn-edge:   #46166b !important;
    --btn-ghost:  #46166b !important;
    --gold-ink:   #8a6508 !important;
    --alert:      #a3261b !important;
    color-scheme: light !important;
  }
  body { background: #fff !important; color: #000 !important; }
  .actions, .masthead { display: none; }
}

/* ------------------------------------------------------------------ theme */

/* Light, dark, or follow the machine.
 *
 * Auto is the default and stays the default: an unset preference means the
 * page follows prefers-color-scheme, which is right for almost everybody. The
 * control exists for the case the machine setting gets wrong — a bright room
 * on a laptop still set to dark, a projector, a student filling this in on a
 * phone in a lecture theatre with the lights down.
 *
 * The choice is kept in localStorage and NOWHERE ELSE. It never reaches the
 * server, so it is not a new fact stored about a person, and it shares no key
 * with anything. That matters more here than it would elsewhere: the whole
 * system is built so that what a student did and what a student wrote cannot
 * be lined up, and a preference written to a row against a session would be a
 * small hole in exactly that wall for the sake of a colour.
 */
.theme {
  display: flex;
  align-items: center;
  gap: 0.1rem;
  margin-left: auto;
  padding: 0.15rem;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  flex: none;
}
.theme button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #ffffff;
  background: transparent;
  border: 0;
  border-radius: 6px;
  /* Square for the two icons; the word widens its own. */
  min-width: 30px;
  min-height: 26px;
  padding: 0 0.3rem;
  cursor: pointer;
  opacity: 0.7;
}
.theme button:hover { opacity: 1; }
.theme button[aria-pressed="true"] {
  opacity: 1;
  color: var(--purple);
  background: #ffffff;
}
.theme svg { display: block; }
.theme .theme-word { padding: 0 0.25rem; font-weight: 600; }

/* The supplementary box under "what most needs improvement". Set apart from the
   routed follow-up above it so that it reads as a second, optional question
   rather than as more of the same one. */
.also { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--rule-soft); }
.also .note { margin: 0 0 0.5rem; }


/* ---------------------------------------------------- fields and controls

   Also moved from admin.css. A search box on the faculty portal and a date
   box in the console are the same control, and were two until the portal
   grew one — at which point `class="field field-wide"` did nothing there,
   because the class lived in a stylesheet that hostname does not serve.
   --------------------------------------------------------------------- */

.actions-row { display: flex; gap: 0.75rem; margin: 1rem 0; }
/* One set of field widths, rather than nine inline ones that never lined up. */
.field { display: block; }
.field-name  { width: 100%; max-width: 14rem; }
.field-email { width: 100%; max-width: 18rem; }
.field-date  { width: 100%; max-width: 11rem; }
.field-wide  { width: 100%; max-width: 26rem; }
.actions-row { flex-wrap: wrap; align-items: center; }

/* A date box with its word above it, in a row of two.
   The console's .option is a full-width control for a question with one
   answer; two dates side by side want their own label, and a date input left
   to itself collapses to the width of its (empty) value. */
.field-label {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: var(--step--1);
  color: var(--muted);
}

/* An input carrying `.option` is a control, not the label wrapper .option is
   written for. That wrapper is a flex row; on an <input> the border is right
   and the flex is not. */
input.option, select.option, textarea.option { display: block; }

/* ========================================================================
   THE SHARED WORKING VOCABULARY — cards, panes, tables.

   MOVED here from admin.css on 20 September 2026, not copied. The console
   loads styles.css and then admin.css, so there is exactly one definition of
   a card and both surfaces use it.

   It was written for the console and the reasoning below is still the
   console's, which is why the comments talk about seven-column tables and
   the Administer list. It is kept verbatim: the argument for why a course is
   a card rather than a row is the same argument on the faculty portal, and
   rewriting it to sound general would lose the case that produced it.

   The faculty portal now presents its courses and its class the same way, so
   somebody who administers a course and somebody who teaches it are looking
   at one design rather than at two that resemble each other. That is only
   safely true while there is one copy — which is the whole reason for the
   move, and the reason not to reintroduce a second one here.
   ======================================================================== */

/* The width every block in a working surface shares. The console widens
   --content to the window; the portal's section pages widen to this. Prose is
   unaffected: it stays at --measure wherever the page ends. */
:root { --panel: min(76rem, 100%); }

/* Tables.
 *
 * min-width was 32rem, which is narrower than the console — so the table never
 * scrolled, it squeezed: six columns fighting over the space, "Accounting for
 * Managers" broken across three lines and the action column clipped. A table
 * should be as wide as its content needs and scroll when that exceeds the page,
 * rather than compressing until nothing is legible. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 1rem 0;
  /* A hint that there is more to the right, rather than a silent clip. */
  /* A fade at each edge while there is more to scroll to, drawn from the accent
     rather than from black — two soft black shadows were invisible against a
     dark background, which is where this console is usually read. */
  background:
    linear-gradient(to right, var(--bg), transparent) 0 0,
    linear-gradient(to left, var(--bg), transparent) 100% 0,
    linear-gradient(to right, color-mix(in srgb, var(--accent) 28%, transparent), transparent) 0 0,
    linear-gradient(to left, color-mix(in srgb, var(--accent) 28%, transparent), transparent) 100% 0;
  background-repeat: no-repeat;
  background-size: 2.5rem 100%, 2.5rem 100%, 0.75rem 100%, 0.75rem 100%;
  background-attachment: local, local, scroll, scroll;
}
.table-wrap > table { margin: 0; }

/* A minimum width belongs to a DATA table — several columns, a header row, one
   row per course — and not to every table on the page. Applied to all of them
   it forced the two-column cohort list to 44rem, so on a narrower console that
   list scrolled sideways and the cohort names disappeared off the left while
   "14 students" sat alone on the right.
   A header row is the discriminator: label-and-value tables do not have one. */
/* A table is as wide as it needs to be, never wider.
 *
 * width: 100% stretched five columns across a 1500px window: the code pinned
 * to the left margin, the enrolment pinned to the right, and a field of empty
 * space between them that the eye has to cross to read one row. Sizing to
 * content keeps a row readable as a row, and leaves the spare width at the
 * right-hand side where it costs nothing. */
/* ONE WIDTH FOR EVERYTHING IN A PANEL.
 *
 * Tables used to size to their content while cards spanned the panel, so a
 * course card and the table beneath it ended at two different places on the
 * right and the mismatch read as a mistake — because it was one.
 *
 * Sizing to content was itself a fix, for a real problem: at the console's
 * full 100rem a five-column table put the course code against the left margin
 * and the enrolled count against the right with a foot of nothing between
 * them, and the eye lost the row crossing it. A shared cap solves that too,
 * and solves the alignment as well: 76rem is wide enough for a card with two
 * panes side by side and narrow enough that a four-column table still reads
 * as a row. Everything in a panel now shares one right edge. */
.card, .table-wrap, .chosen, .summary { max-width: var(--panel); }
.table-wrap > table { width: 100%; max-width: 100%; }
.table-wrap > table:has(thead) { min-width: min(44rem, 100%); }
.table-wrap > table:not(:has(thead)) { min-width: 0; }
/* Columns get breathing room from padding rather than from stretching. */
.table-wrap > table:has(thead) th,
.table-wrap > table:has(thead) td { padding-right: 1.75rem; }
.table-wrap > table:has(thead) th:last-child,
.table-wrap > table:has(thead) td:last-child { padding-right: 0.6rem; }

/* Column shapes for data tables, so the browser stops guessing. The first
   column carries a course code and its title and needs the most room; dates and
   counts need almost none and should not be handed a third of the table. */
.table-wrap > table:has(thead) th,
.table-wrap > table:has(thead) td { white-space: normal; }
.table-wrap > table:has(thead) th:first-child,
.table-wrap > table:has(thead) td:first-child { min-width: 13rem; }
/* The action column wraps.
 *
 * nowrap put four controls — Open Report, Download Package, Data Only,
 * Distribute — on a single line, which pushed the table past its container and
 * clipped the last one out of sight. A row of actions should wrap onto a second
 * line rather than run off the page. */
.table-wrap > table:has(thead) th:last-child { white-space: nowrap; text-align: left; }
.table-wrap > table:has(thead) td:last-child {
  white-space: normal;
  text-align: left;
  /* Wide enough that two controls sit side by side rather than stacking one
     per line, which turned a row into a column of buttons. */
  min-width: 16rem;
}
.table-wrap > table:has(thead) td:last-child .btn {
  margin: 0 0.35rem 0.35rem 0;
  vertical-align: top;
}
/* Every cell sits on the same top line, the action included — a button
   floating on the vertical centre of a two-line row reads as misaligned. */
.table-wrap td, .table-wrap th { vertical-align: top; }
.table-wrap td .btn { margin-top: 0; }
.nowrap { white-space: nowrap; }

.table-wrap > table:has(thead) td:nth-child(n+2):nth-child(-n+5) { white-space: nowrap; }


table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: var(--step--1); }
th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--rule-soft); vertical-align: top; }
th { color: var(--muted); font-weight: 500; white-space: nowrap; }

/* 6px, the same radius as a button, a tab, an input and a stat tile. It was a
   99px pill, which is the one rounded shape on a screen made of square ones. */
.tag {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 6px;
  font-size: var(--step--1);
}
.tag.add       { background: var(--accent-bg); color: var(--ink); border: 1px solid var(--accent); }
.tag.update    { background: var(--specimen); color: var(--ink-soft); }
.tag.unchanged { color: var(--muted); }
.tag.skip,
.tag.issue     { background: var(--btn-bg); color: var(--btn-ink); }


/* The Administer list was the first: a card per course, not a row per course.
 *
 * It was a seven-column table, and two instruments made it unworkable rather
 * than merely tight. Each instrument needs a status, two dates, a count, a
 * progress bar and up to four controls, and a table gives it one cell about
 * fourteen characters wide — so a sentence explaining an overtaken midpoint
 * check wrapped to six lines and dragged its whole row down, while "Open the
 * Midpoint Check" broke across three. No column width fixes that; the shape
 * was wrong.
 *
 * Facts across the top, where they read as one line. The two instruments side
 * by side beneath, each with room to say what state it is in. Scanning is not
 * lost: the groups above — Open Now, Ready to Administer — are what somebody
 * is actually scanning for, and within a group there are only ever a few.
 */
.card-group { margin: 0 0 2.25rem; }
.card-group > h3 { margin-top: 0; }
.card-group > .note { margin-bottom: 1rem; }

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 10px;
  margin: 0 0 0.85rem;
  overflow: hidden;
}

.card-head { padding: 1rem 1.15rem 0.9rem; }

.card-id {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.7rem;
  margin: 0 0 0.6rem;
  font-family: var(--serif);
  font-weight: 600;
  line-height: 1.25;
}
.card-code  { font-size: var(--step-1); }
.card-title { font-size: var(--step-0); color: var(--ink-soft); font-weight: 400; }

/* The facts as label-over-value pairs rather than as a row of a table. Same
   information, and it no longer has to line up with sixteen other courses —
   which is what forced every column to the width of its longest member. */
.card-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2rem;
  margin: 0;
  font-size: var(--step--1);
}
.card-facts dt {
  color: var(--muted);
  font-size: 0.6875rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  margin: 0 0 0.1rem;
}
.card-facts dd { margin: 0; color: var(--ink); }
/* A fact can be a short list — every spelling a name has been exported under.
   Left to itself it pushed the row to the width of the longest one. */
.card-facts dd { max-width: 22rem; }
.card-facts .sub { display: block; color: var(--muted); }

/* Two instruments, side by side, stacking when there is no room for two. */
.card-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  border-top: 1px solid var(--rule);
}
.pane { padding: 0.9rem 1.15rem 1rem; min-width: 0; }
/* A field and its button on one line inside a pane, wrapping when there is no
   room. The email input was given a cell the width of the word "Address" when
   these lived in a table. */
.pane .acts .field { flex: 1 1 12rem; min-width: 0; }
/* A rule between the two, and only between: a border on every panel draws a
   box around each and turns one card into two. */
.pane + .pane { border-left: 1px solid var(--rule); }
@media (max-width: 40rem) {
  .pane + .pane { border-left: 0; border-top: 1px solid var(--rule); }
}

.pane-name {
  margin: 0 0 0.55rem;
  font-family: var(--sans);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.pane-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.35rem;
  max-width: none;
  font-size: var(--step--1);
}
.pane-state .st { margin: 0; }
.pane-dates { color: var(--muted); }
.pane-count { margin: 0 0 0.35rem; font-size: var(--step--1); max-width: none; }
.pane-count strong { font-size: var(--step-0); }

/* The explanation under a state. Narrow on purpose — it is a footnote to the
   panel, not the panel. */
.pane-why {
  margin: 0.3rem 0 0;
  font-size: var(--step--1);
  color: var(--muted);
  max-width: 26rem;
}

.pane .bar { margin: 0.35rem 0 0.7rem; max-width: none; }

/* Controls wrap onto a second line rather than running out of the panel, and
   they sit on one baseline rather than stacking one per line. */
/* 0.4rem had four outlined buttons reading as one strip of chrome rather than
   as four separate things to choose between. They are four different actions
   with four different consequences — one opens a page, one downloads a
   personnel document — and they should not look like a segmented control. */
.acts { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 0.8rem; }
.acts .btn {
  min-height: 36px;
  padding: 0.35rem 0.8rem;
  font-size: var(--step--1);
  white-space: nowrap;
}
.acts input { min-height: 36px; }
/* A detach control names the spelling it removes, so those labels are as long
   as a person's name and must be allowed to wrap. */
.pane .acts .btn[data-unlink] { white-space: normal; text-align: left; }

/* A panel with nothing to do in it, and a course already finished. Legible,
   visibly not the thing to look at. */
.pane-quiet .pane-state > span:not(.st) { color: var(--muted); }
.card.spent { opacity: 0.62; }
.card.spent:hover { opacity: 1; }

/* ------------------------------------------------------------------------
   The Faculty Experience Survey.

   The one thing on the faculty portal that asks the reader to DO something
   rather than to read something, so it is marked the way the student site
   marks its odd card out: gold on the edge, purple everywhere else. Gold is
   struck once on a page and this is that once — the portal's other cards are
   evaluations to open, and none of them appears in this section.
   ------------------------------------------------------------------------ */

.asks { margin: 0 0 2rem; }
.asks > h2 { margin-bottom: 0.3rem; }
.asks > .note { margin-top: 0; }

.course-card.fes-ask { border-left: 3px solid var(--gold); }
.course-card.fes-ask .instrument { color: var(--gold-ink); }

/* The form itself.
   A survey rather than a report, so it uses the student instrument's
   fieldsets and options unchanged; only the three things it has and they do
   not are defined here. */

/* The reporting year under the heading. Metadata, not a course. */
.course-meta {
  margin: -0.4rem 0 1.2rem;
  color: var(--muted);
  font-size: var(--step--1);
}

/* The instruction above a section, which is guidance for answering rather
   than a note about the system — so it sits with the questions rather than
   in the grey the notes use. */
.section-note {
  margin: 0 0 1rem;
  font-style: italic;
  color: var(--ink);
}

/* An always-open comment box under a scale. Indented so the pair reads as
   one item, which is what it is. */
.follow-open {
  /* The bottom margin matches .question's own, which is the only spacing
     between items on this form — .question sets margin-bottom and no
     margin-top, so a block sitting between two of them has to carry the gap
     itself or the next question butts straight up against this box. */
  margin: 0.4rem 0 2rem 0.2rem;
  padding-left: 0.85rem;
  border-left: 2px solid var(--rule);
}
.prompt.subtle {
  font-weight: 400;
  font-size: var(--step--1);
  color: var(--muted);
}

/* Taking an answer back.
   Every question here is optional and a radio cannot be unchecked, so
   without this a mis-click is an answer the instructor did not mean to give.
   Quiet: it should be findable when wanted and invisible otherwise. */
.clear-answer {
  display: inline-block;
  margin: 0.15rem 0 0;
  padding: 0.1rem 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--step--1);
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
}
.clear-answer:hover { color: var(--accent); }
/* `hidden` is enforced here rather than left to the browser. The user-agent
   rule for [hidden] loses to ANY author `display`, so the declaration above
   was showing the button on every question with nothing chosen. */
.clear-answer[hidden] { display: none; }

