/* Brandy Luther — portfolio
   Hand-written, no framework, no external requests, no webfonts.
   Light/dark via prefers-color-scheme; print stylesheet at the end. */

:root {
  --paper:      #faf7f2;
  --paper-2:    #f2ece1;
  --ink:        #221f1a;
  --ink-soft:   #55504a;
  --ink-faint:  #6b655a;   /* AA contrast on --paper and --paper-2 */
  --rule:       #e0d8c9;
  --accent:     #7a2e2e;   /* oxblood — restrained, academic */
  --accent-2:   #1d5c58;   /* deep teal for links */
  --measure:    68ch;
  --serif: Iowan Old Style, Palatino Linotype, Palatino, Georgia, "Times New Roman", serif;
  --sans:  ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:     #14130f;
    --paper-2:   #1d1b16;
    --ink:       #ece7dd;
    --ink-soft:  #b8b1a5;
    --ink-faint: #9a938a;
    --rule:      #302c25;
    --accent:    #d98a8a;
    --accent-2:  #7fc9c2;
  }
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

.wrap { width: 100%; max-width: 46rem; margin-inline: auto; padding-inline: 1.35rem; }

.skip {
  position: absolute; left: -9999px;
  background: var(--accent); color: #fff;
  padding: .7rem 1rem; z-index: 10; border-radius: 0 0 6px 0;
}
.skip:focus { left: 0; top: 0; }

/* ---------- hero ---------- */

.hero {
  background: var(--paper-2);
  border-bottom: 1px solid var(--rule);
  padding: clamp(3rem, 9vw, 5.5rem) 0 clamp(1.6rem, 3vw, 2.2rem);
}

.eyebrow {
  margin: 0 0 .9rem;
  font-size: .74rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600;
}

h1 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2.3rem, 7.5vw, 3.6rem);
  line-height: 1.08;
  letter-spacing: -.015em;
  margin: 0 0 1rem;
}
h1 .creds { color: var(--accent); font-size: .5em; letter-spacing: .02em; white-space: nowrap; }

.tagline {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 2.4vw, 1.25rem);
  color: var(--ink-soft);
  max-width: var(--measure);
  margin: 0 0 2rem;
}

.hero nav { display: flex; flex-wrap: wrap; gap: .4rem 1.4rem; }
.hero nav a {
  color: var(--ink-soft); text-decoration: none;
  font-size: .84rem; letter-spacing: .05em; text-transform: uppercase; font-weight: 600;
  padding: .25rem 0; border-bottom: 2px solid transparent;
}
.hero nav a:hover, .hero nav a:focus-visible { color: var(--accent); border-bottom-color: var(--accent); }

/* ---------- sections ---------- */

main { padding-block: clamp(2.5rem, 6vw, 4rem); }

section { margin-bottom: clamp(3rem, 7vw, 4.5rem); scroll-margin-top: 1.5rem; }

h2 {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(1.5rem, 4vw, 1.95rem);
  margin: 0 0 1.5rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--rule);
  letter-spacing: -.01em;
}

h3 { font-size: 1.06rem; margin: 0 0 .3rem; line-height: 1.35; font-weight: 650; }
h3 .at { font-weight: 400; color: var(--ink-soft); }

p { max-width: var(--measure); margin: 0 0 1rem; }
.lede { font-family: var(--serif); font-size: 1.14rem; color: var(--ink); }
.sub  { color: var(--ink-faint); font-size: .93em; }

a { color: var(--accent-2); text-underline-offset: 2px; }

/* ---------- roles ---------- */

.role { padding-left: 1.15rem; border-left: 2px solid var(--rule); margin-bottom: 2.4rem; }
.role:hover { border-left-color: var(--accent); }
.role-head { margin-bottom: .75rem; }

.meta { font-size: .85rem; color: var(--ink-faint); margin: 0; }
.meta .dot { margin-inline: .5rem; }
.meta time { white-space: nowrap; }

.role ul { margin: 0; padding-left: 1.1rem; }
.role li { margin-bottom: .45rem; max-width: var(--measure); }
.role li::marker { color: var(--ink-faint); }

/* ---------- degrees ---------- */

.degrees { list-style: none; margin: 0; padding: 0; }
.degrees li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .15rem 1.5rem;
  padding: .95rem 0;
  border-bottom: 1px solid var(--rule);
}
.degrees li:last-child { border-bottom: 0; }
.deg  { font-weight: 650; margin: 0; }
.inst { grid-column: 1; color: var(--ink-soft); font-size: .93rem; margin: 0; }
.when {
  grid-column: 2; grid-row: 1;
  color: var(--ink-faint); font-size: .85rem; white-space: nowrap; text-align: right; margin: 0;
}

/* ---------- research ---------- */

.pub { padding: 1.1rem 0; border-bottom: 1px solid var(--rule); }
.pub:last-child { border-bottom: 0; }
.pub.featured {
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}
.pub h3 { font-family: var(--serif); font-weight: 400; font-size: 1.16rem; line-height: 1.3; }
.cite { font-size: .85rem; color: var(--ink-faint); margin-bottom: .8rem; }

/* ---------- teaching tags ---------- */

.tags { list-style: none; display: flex; flex-wrap: wrap; gap: .5rem; padding: 0; margin: 0 0 1.4rem; }
.tags li {
  font-size: .84rem;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: .3rem .8rem;
  color: var(--ink-soft);
}

/* ---------- service ---------- */

.cols { display: grid; gap: 2rem; }
@media (min-width: 40rem) { .cols { grid-template-columns: 1fr 1fr; } }
.cols h3 {
  font-size: .78rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: .7rem;
}
.cols ul { margin: 0; padding-left: 1.05rem; }
.cols li { margin-bottom: .5rem; font-size: .95rem; }

/* ---------- talk to brandy ---------- */

/* Single column since the portrait was removed (2026-09-01). It was a two-column grid
   `minmax(9rem, 15rem) 1fr` with the photo in column 1; dropping the <img> without this
   would leave a ~15rem empty column beside the text. */
.talk-card {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 1.5rem;
  align-items: start;
}
.talk-body p { max-width: none; }
.talk-note { color: var(--ink-soft); font-size: .95rem; }

/* Loading indicator shown while the models warm. The avatar unloads when idle, so a
   visitor who clicks after a quiet spell waits ~40s — silence there reads as a hang. */
.warm-note {
  display: flex; align-items: center; gap: .55rem;
  margin: .9rem 0 0; color: var(--ink-faint); font-size: .9rem;
}
.warm-note .spin {
  width: .85rem; height: .85rem; flex: none;
  border: 2px solid var(--rule); border-top-color: var(--accent);
  border-radius: 50%; animation: warmspin .8s linear infinite;
}
@keyframes warmspin { to { transform: rotate(360deg); } }
/* A spinning ring is motion for its own sake; honour the preference and keep the text. */
@media (prefers-reduced-motion: reduce) {
  .warm-note .spin { animation: none; border-top-color: var(--rule); }
}
.talk-card .btn { cursor: pointer; }
.talk-card .btn:disabled { opacity: .55; cursor: default; }
@media (max-width: 34rem) {
  .talk-card { grid-template-columns: 1fr; }
}

/* ---------- contact ---------- */

.contact-row { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1.4rem; }
.btn {
  display: inline-block;
  background: var(--accent); color: #fff;
  padding: .7rem 1.3rem; border-radius: 6px;
  text-decoration: none; font-weight: 600; font-size: .95rem;
  border: 1px solid var(--accent);
}
.btn:hover, .btn:focus-visible { filter: brightness(1.12); }
.btn.ghost { background: transparent; color: var(--accent); }
@media (prefers-color-scheme: dark) { .btn { color: #14130f; } }
.fallback { font-size: .9rem; color: var(--ink-faint); }

:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; }

footer {
  border-top: 1px solid var(--rule);
  padding-block: 1.8rem 3rem;
  color: var(--ink-faint); font-size: .85rem;
}

/* ---------- print ---------- */

@media print {
  :root {
    --paper: #fff; --paper-2: #fff; --ink: #000;
    --ink-soft: #333; --ink-faint: #555; --rule: #bbb;
    --accent: #000; --accent-2: #000;
  }
  body { font-size: 10.5pt; line-height: 1.42; }
  .hero { padding: 0 0 .6rem; border-bottom: 2px solid #000; }
  .hero nav, .skip, .fallback, footer, #talk { display: none !important; }
  /* .contact-row must print: the email is injected into #email-link by JS and
     the .fallback copy is inside <noscript>, so hiding both left a printed
     page with no way to reach her at all. Render the buttons as plain text. */
  .btn { background: none; border: 0; padding: 0; font-weight: 400; }
  .contact-row { gap: 1.5rem; margin-top: .5rem; }
  .wrap { max-width: none; padding-inline: 0; }
  main { padding-block: .8rem; }
  h1 { font-size: 21pt; margin-bottom: .3rem; }
  .tagline { font-size: 10.5pt; margin-bottom: .4rem; }
  h2 { font-size: 12.5pt; margin: 0 0 .6rem; }
  section { margin-bottom: 1.1rem; page-break-inside: avoid; }
  .role, .pub, .degrees li { page-break-inside: avoid; }
  .pub.featured { background: none; border: 1px solid #bbb; padding: .7rem; }
  .role li { margin-bottom: .18rem; }
  a { color: #000; text-decoration: none; }
}

/* ---------- chat ---------- */

.vh {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.chat { margin-top: 1.2rem; }
.chat-log {
  max-height: 22rem; overflow-y: auto;
  display: flex; flex-direction: column; gap: .6rem;
  margin-bottom: .9rem;
}
.chat-log:empty { display: none; }
.msg {
  padding: .6rem .85rem; border-radius: 10px;
  font-size: .95rem; line-height: 1.55; max-width: 46ch;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.msg-you { align-self: flex-end; background: var(--accent); color: #fff; }
@media (prefers-color-scheme: dark) { .msg-you { color: #14130f; } }
.msg-her { align-self: flex-start; background: var(--paper); border: 1px solid var(--rule); }
.chat-form { display: flex; gap: .6rem; flex-wrap: wrap; }
.chat-form input {
  flex: 1 1 14rem; min-width: 0;
  padding: .65rem .8rem; font: inherit; font-size: .95rem;
  color: var(--ink); background: var(--paper);
  border: 1px solid var(--rule); border-radius: 6px;
}
.chat-form input::placeholder { color: var(--ink-faint); }
.chat-form input:disabled { opacity: .6; }
.chat-form .btn { cursor: pointer; }
.chat-form .btn:disabled { opacity: .55; cursor: default; }

/* Secondary action next to the primary one — voice sits beside text chat, not
   above it, because text is the fallback rung when a mic or browser can't. */
.btn-quiet { background: transparent; color: var(--accent); border: 1px solid var(--rule); }
.btn-quiet:hover, .btn-quiet:focus-visible { filter: none; background: var(--paper); }
@media (prefers-color-scheme: dark) { .btn-quiet { color: var(--accent); } }

/* Live mic level. Without it, "it can't hear me" is unfalsifiable from the user's side. */
.mic-meter { display: block; margin-top: .35rem; font-size: .85rem; color: var(--ink-soft);
             font-variant-numeric: tabular-nums; letter-spacing: .05em; min-height: 1.2em; }
#mic-pick { display: block; margin-top: .4rem; max-width: 28rem; font: inherit;
            font-size: .85rem; padding: .3rem .4rem; border: 1px solid var(--rule);
            border-radius: 4px; background: var(--paper); color: var(--ink); }

/* The face. Fixed aspect so the canvas swap does not reflow the page. */
.face { margin: 0 0 1rem; max-width: 320px; }
.face video, .face canvas { width: 100%; display: block; border-radius: 8px;
                            background: var(--paper); aspect-ratio: 1 / 1; }
/* `display: block` above beats the UA's `[hidden] { display: none }`, so without this
   the idle loop and the live canvas both render and you see TWO faces stacked. */
.face video[hidden], .face canvas[hidden] { display: none; }
.face-note { font-size: .75rem; color: var(--ink-faint); margin-top: .35rem; }

/* ---------------------------------------------------------------------------
   PRINT: this stylesheet is what generates cv.pdf.

   cv.pdf used to be a SEPARATE artefact, printed from an older CV-only page.
   That let it drift: on 2026-08-29 the PDF was still publishing "Dean's List and
   Chancellor's List (2012)" and "British emphasis" months after canonical_facts.md
   marked both do-not-assert, because nothing regenerated it and no gate read it.

   Now the PDF is printed from THIS page, so the CV cannot say anything the page
   does not -- and gate_site_facts.py checks the page. One source, one gate.

   Hide only the interactive layer; the CV content itself is the same markup a
   visitor reads. Keep this list in step with the #talk section's controls.
   --------------------------------------------------------------------------- */
@media print {
  #talk,                      /* the whole avatar + chat widget */
  #face, #face-idle, #face-live,
  #chat, #chat-form, #chat-log,
  .btn, .talk-card,
  nav, .nav, .sitenav,
  #voice-start, #voice-interrupt, #talk-start, #chat-send, #mic-pick,
  #email-link { display: none !important; }

  /* The contact section still matters on a CV -- keep the text, drop the buttons
     (a "Download CV" button inside the CV is circular, and "Email me" is a link
     to nowhere on paper; the address itself is printed in the contact block). */
  a[href$="cv.pdf"] { display: none !important; }

  body { background: #fff; color: #000; }
  a { color: #000; text-decoration: none; }
  section { break-inside: auto; page-break-inside: auto; }
  h2 { break-after: avoid; page-break-after: avoid; }
}
