/* The CV, migrated from the SvelteKit app. The layout, the A4 sheet metaphor
   and the animation timings are the original's; the palette is the site's.

   Every colour here resolves from the four site tokens (--color-text,
   --color-background, --color-border, --color-marker), so the CV follows the
   theme sandbox in dev along with the rest of the pages. The fonts are the
   CV's own and stay that way: it is a document, and it should not read as a
   note. */
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Playpen+Sans:wght@100..800&family=Space+Grotesk:wght@500;600;700&display=swap");

.cv-body {
  /* The one place the theme lands, and it lands on the same value the rest of
     the site uses, so arriving here from a note is not a change of ground.
     Everything the CV prints on keeps the default palette below: a printed page
     does not change colour with the room it is read in.

     On the default theme the desk and the sheet are therefore the same cream,
     and the sheet is read from its border and shadow alone. That is the trade
     for matching the rest of the site exactly. */
  background: var(--color-background);
}

.cv-shell {
  /* The site's default palette, written out rather than read from the theme
     tokens. The sheet and the bubble are paper, and paper does not take the
     theme; only the desk under them does (see .cv-body above). Keep these in
     step with the :root defaults in site.css. */
  --cv-ink: #333331;
  --cv-sheet: #fffdf4;
  --cv-line: #d7d3c8;
  --cv-muted: color-mix(in srgb, var(--cv-ink) 58%, var(--cv-sheet));
  --cv-faint: color-mix(in srgb, var(--cv-ink) 34%, var(--cv-sheet));
  /* Fixed too: under a light-on-dark theme a text-derived shadow becomes a
     glow, and the sheet stops sitting on the desk and starts hovering over it. */
  --cv-shadow: rgba(0, 0, 0, .13);
  /* The original swept a dusty rose under hovered titles. Marigold is the one
     accent on this site, so it takes that job here too. */
  --cv-sweep: color-mix(in srgb, var(--color-marker) 72%, transparent);

  --cv-font-body: "IBM Plex Sans", system-ui, sans-serif;
  --cv-font-display: "Space Grotesk", system-ui, sans-serif;
  --cv-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --cv-font-hand: "Playpen Sans", system-ui, sans-serif;

  font-family: var(--cv-font-body);
  font-size: 11px;
  line-height: 1.4;
  padding: 50px 8px;
  min-height: 100vh;
  overflow-x: hidden;
}

/* The sheet is drawn at A4 and then scaled, rather than sized in rem. That is
   what keeps the print layout and the screen layout the same layout. */
.cv-content {
  margin: 320px auto 0;
  position: relative;
  z-index: 1;
}

/* `zoom` rather than `transform: scale`, which is what the original used. A
   transform does not feed back into layout, so the document stopped at the
   sheet's unscaled height and the last third of the CV could not be scrolled
   to. Zoom scales the used values, so the page grows with it. */
@media (min-width: 1024px) {
  .cv-shell { padding: 200px 0; }
  .cv-content { zoom: 1.5; }
}

@media (max-width: 1023px) { .cv-shell { font-size: 14px; } }
@media (max-width: 680px) { .cv-shell { padding-top: 160px; } .cv-content { margin-top: 390px; } }
@media (max-width: 550px) { .cv-shell { font-size: 15px; } }

.cv-page {
  position: relative;
  width: 21cm;
  height: 29.7cm;
  margin: 0 auto 1.5cm;
  background: var(--cv-sheet);
  color: var(--cv-ink);
  border: 2px solid var(--cv-ink);
  border-radius: 8px;
  box-shadow:
    0 1px 1px var(--cv-shadow),
    0 2px 2px var(--cv-shadow),
    0 4px 4px var(--cv-shadow),
    0 6px 8px var(--cv-shadow),
    0 8px 16px var(--cv-shadow);
}

@media (max-width: 800px) {
  .cv-page { width: auto; height: auto; }
}

/* Back link. Shares the note pages' treatment, so leaving the CV feels like
   leaving a note. It stays pinned at every width, unlike on a note where it
   drops into the flow: here the flow starts with a speech bubble hanging in
   mid-air, and an arrow parked above that reads as debris. */
.cv-shell .back-link { position: fixed; top: 30px; margin-bottom: 0; z-index: 2; }

/* The ASCII knot behind the desk. Fixed under the sheet (.cv-shell sits at
   z-index: 1), and its colour is mixed from the theme tokens rather than
   stated, so on ultramarine it is a quiet lighter blue and it keeps working
   if the theme moves. The table the effect builds inherits this colour. */
.ascii-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  color: color-mix(in srgb, var(--color-text) 30%, var(--color-background));
  animation: ascii-bg-in .5s ease-out both;
}
@keyframes ascii-bg-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .ascii-bg { animation: none; }
}

/* Work entries have their gallery disabled for now (see cv.html), so the
   click affordances are hidden with it. */
#work .entry .glyph { display: none; }
#work .entry .sweep { background: none; }

/* Columns */
.cv-columns {
  display: grid;
  gap: 30px 50px;
  grid-template-columns: 1.9fr 1fr;
  padding: 36px;
}

.cv-page article { margin-bottom: 28px; }
.cv-page ul { list-style: none; margin: 0; padding: 0; }
.cv-page p { margin: 0; }

.cv-page h2 {
  font-family: var(--cv-font-display);
  font-size: 1.1em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  line-height: 1.3;
  color: var(--cv-ink);
  margin: 0 0 12px;
  padding-bottom: 2px;
  border-bottom: 2px solid var(--cv-ink);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

.cv-page h2 .aside {
  font-family: var(--cv-font-mono);
  font-size: .7em;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: lowercase;
  color: var(--cv-muted);
}

/* Entries. A clickable one carries `data-media` and opens the gallery; the
   sweep and the glyph are what say so, since a cursor change alone is
   invisible until you are already on it. */
.entry { margin: 16px 0 24px; }
#education .entry { margin: 8px 0 12px; }
#education .entry:last-child { margin-bottom: 0; }

[data-media] { cursor: pointer; }
[data-media]:focus-visible { outline: none; }

.sweep {
  background: linear-gradient(180deg, transparent 65%, var(--cv-sweep) 65%);
  background-repeat: no-repeat;
  background-size: 0 100%;
  background-position: 0 0;
  transition: background-size .35s cubic-bezier(.2, .8, .2, 1);
}

[data-media]:hover .sweep,
[data-media]:focus-visible .sweep { background-size: 100% 100%; }

.glyph {
  display: inline-block;
  margin-left: 4px;
  font-size: .85em;
  color: var(--cv-muted);
  opacity: .45;
  transition: opacity .2s ease, transform .2s ease;
}

[data-media]:hover .glyph,
[data-media]:focus-visible .glyph { opacity: 1; transform: translateX(2px); }

.entry-title {
  font-size: 1.1em;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--cv-ink);
}

.entry-where {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  font-family: var(--cv-font-mono);
  font-size: .85em;
  color: var(--cv-muted);
  margin-bottom: 4px;
}

.entry-where > span { font-size: .95em; }

.entry-date { margin-left: 6px; white-space: nowrap; }
.entry-date::before { content: "·"; margin-right: 6px; opacity: .6; }
.entry-date .to::before { content: "–"; padding: 0 3px; }
.entry-date .to:empty { display: none; }

.entry-details {
  margin-top: 3px;
  font-size: 1em;
  line-height: 1.6;
  color: var(--cv-ink);
}

/* Languages and toolkit */
.languages { display: flex; flex-wrap: wrap; gap: 8px 12px; }
.languages li { display: inline-flex; align-items: baseline; gap: 6px; }
.language-name { font-weight: 600; }
.language-level { font-family: var(--cv-font-mono); font-size: .85em; color: var(--cv-muted); }

.tools { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.tools li { font-family: var(--cv-font-mono); font-size: .9em; color: var(--cv-muted); }

/* Teaching */
.teaching { margin-bottom: 10px; font-size: .95em; line-height: 1.5; }
.teaching:last-child { margin-bottom: 0; }

/* Personal projects */
#personal-projects { margin-top: 24px; }
.projects .project { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 16px; }
.projects .project:last-child { margin-bottom: 0; }

.project-thumb {
  flex: none;
  width: 60px;
  height: 60px;
  object-fit: cover;
  border-radius: 3px;
  border: 1px solid var(--cv-line);
}

.project-body { flex: 1; display: flex; flex-direction: column; gap: 3px; }

.project-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}

.project-header strong { font-weight: 600; font-size: 1.05em; letter-spacing: -.01em; line-height: 1; }

.project-dates {
  font-family: var(--cv-font-mono);
  font-size: .85em;
  color: var(--cv-muted);
  white-space: nowrap;
  line-height: 1;
}

.project-dates .to::before { content: "–"; padding: 0 3px; }
.project-dates .to:empty { display: none; }

.project-dates::after {
  content: " ▸";
  display: inline-block;
  margin-left: 4px;
  opacity: .45;
  transition: opacity .2s ease, transform .2s ease;
}

.project:hover .project-dates::after,
.project:focus-visible .project-dates::after { opacity: 1; transform: translateX(2px); }

.project-description { font-size: .8em; line-height: 1.4; color: var(--cv-ink); }

/* The speech bubble. It hangs above the sheet, which is what the 320px top
   margin on .cv-content is reserving room for. */
.hello {
  position: absolute;
  top: -248px;
  left: 15%;
  right: 0;
  height: 300px;
  pointer-events: none;
}

.chat-bubble {
  position: absolute;
  bottom: 80px;
  left: 110px;
  width: 350px;
  padding: 16px 6px 0 16px;
  background: var(--cv-sheet);
  color: var(--cv-ink);
  border: 2px solid var(--cv-ink);
  border-radius: 8px;
  filter: drop-shadow(0 2px 4px var(--cv-shadow));
  font-family: var(--cv-font-hand);
  font-size: 15px;
  line-height: 1.3;
  pointer-events: auto;
  transform-origin: 0 calc(100% - 22px);
  opacity: 0;
}

.chat-bubble::before {
  content: "";
  position: absolute;
  bottom: 22px;
  left: -7px;
  width: 10px;
  height: 10px;
  background: var(--cv-sheet);
  border-left: 2px solid var(--cv-ink);
  border-bottom: 2px solid var(--cv-ink);
  transform: rotate(45deg);
}

.chat-bubble li { display: flex; gap: 6px; margin-bottom: 18px; font-size: .9em; }
.chat-bubble .emoji { padding-right: 6px; }
/* The bubble is the one place on the page that offers links, so they carry the
   marker rather than an underline, matching the homepage contact row. */
.chat-bubble a {
  text-decoration-line: underline;
  text-decoration-color: var(--color-marker);
  text-decoration-thickness: 2px;
  text-underline-offset: .18em;
}

.guy {
  position: absolute;
  top: 160px;
  left: 0;
  width: 150px;
  opacity: 0;
  filter: drop-shadow(2px 2px 2px var(--cv-shadow));
}

/* Entry animation. Gated on `fonts-ready` so the cascade does not play against
   fallback metrics and then reflow under the finished faces. */
.cv-page .col1 > article,
.cv-page .col2 > article { opacity: 0; }

.fonts-ready .guy { animation: cv-guy-in .25s cubic-bezier(.34, 1.56, .64, 1) .35s both; }
.fonts-ready .chat-bubble { animation: cv-bubble-in .18s ease-out .65s both; }

.fonts-ready .cv-page .col1 > article,
.fonts-ready .cv-page .col2 > article { animation: cv-article-in .25s ease-out both; }

.fonts-ready .cv-page .col1 > article:nth-of-type(1) { animation-delay: .90s; }
.fonts-ready .cv-page .col2 > article:nth-of-type(1) { animation-delay: .95s; }
.fonts-ready .cv-page .col2 > article:nth-of-type(2) { animation-delay: 1.00s; }
.fonts-ready .cv-page .col2 > article:nth-of-type(3) { animation-delay: 1.05s; }
.fonts-ready .cv-page .col2 > article:nth-of-type(4) { animation-delay: 1.10s; }
.fonts-ready .cv-page .col2 > article:nth-of-type(5) { animation-delay: 1.15s; }

@keyframes cv-article-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes cv-bubble-in { from { opacity: 0; transform: scale(.92) translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes cv-guy-in { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .cv-page .col1 > article,
  .cv-page .col2 > article,
  .chat-bubble,
  .guy { opacity: 1; animation: none; transform: none; }
}

/* Small screens: the sheet stops being a sheet and becomes a single column. */
@media (max-width: 680px) {
  .hello { left: 0; right: 0; }
  .guy { left: auto; right: 10px; }
  .chat-bubble {
    bottom: 140px;
    left: auto;
    right: 20px;
    width: calc(100% - 40px);
    max-width: 450px;
    padding: 26px 6px 14px 16px;
    font-size: 18px;
  }
  .chat-bubble li { font-size: 1em; margin-bottom: 12px; }
  .chat-bubble::before {
    bottom: -7px;
    left: auto;
    right: 55px;
    border-left: none;
    border-right: 2px solid var(--cv-ink);
  }
}

@media (max-width: 550px) {
  .cv-columns { display: flex; flex-direction: column; padding: 20px; }
  .cv-page h2 { font-size: 1.2em; }
}

/* Print, and the ?pdf preview of it.

   The two blocks carry the same declarations on purpose. A media query cannot
   be switched on by a class and print cannot set one, so there is no way to
   write this once without making print depend on javascript. Change one, change
   the other.

   What changes: the bubble goes and the identity header takes its job, the
   sheet stops being a sheet (no border, no shadow, no fixed A4 box, no zoom)
   and the whole thing tightens up to fit one page. */
.cv-pdf .cv-shell { padding: 0; font-size: 11px; min-height: 0; }
.cv-pdf .cv-content { margin: 0; zoom: 1; }
.cv-pdf .cv-page {
  width: 21cm;
  height: auto;
  margin: 0 auto;
  box-shadow: none;
}
.cv-pdf .hello,
.cv-pdf .glyph,
.cv-pdf .project-dates::after { display: none; }
.cv-pdf .cv-identity { display: flex; }
.cv-pdf .cv-columns { padding: 14px; gap: 18px 40px; }
.cv-pdf .cv-page article { margin-bottom: 22px; }
.cv-pdf .cv-page h2 { margin-bottom: 16px; }
.cv-pdf #personal-projects { margin-top: 0; }
.cv-pdf #education .entry { margin: 8px 0; }
.cv-pdf .cv-page .col1 > article,
.cv-pdf .cv-page .col2 > article { opacity: 1; animation: none; }

@media print {
  @page { margin: .02in; size: A4; }

  .cv-body { background: #fff; }
  .ascii-bg { display: none; }
  .cv-shell { padding: 0; font-size: 11px; min-height: 0; }
  .cv-content { margin: 0; zoom: 1; }
  .cv-page {
    width: auto;
    height: auto;
    margin: 0;
    background: #fff;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  .hello,
  .back-link,
  .glyph,
  .project-dates::after,
  .tweaks { display: none !important; }
  .cv-identity { display: flex; }
  .cv-columns { padding: 14px; gap: 18px 40px; }
  .cv-page article { margin-bottom: 22px; }
  .cv-page h2 { margin-bottom: 16px; }
  #personal-projects { margin-top: 0; }
  #education .entry { margin: 8px 0; }
  .cv-page .col1 > article,
  .cv-page .col2 > article { opacity: 1; animation: none; }
  .cv-identity { padding: 14px 14px 12px; }
}

/* The identity header. Hidden on screen, where the bubble does this job. */
.cv-identity {
  display: none;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 36px 36px 0;
}

.cv-name {
  font-family: var(--cv-font-display);
  font-size: 20px;
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.2;
}

.cv-role { color: var(--cv-muted); font-style: italic; }
.cv-email { color: inherit; text-decoration: none; }
.cv-email:hover { text-decoration: underline; }

/* Media popover. Built by cv.js and appended to <body>, outside .cv-shell,
   because the sheet is scaled at desktop widths and a transformed ancestor
   makes `position: fixed` resolve against it instead of the viewport.
   Being outside also means it cannot inherit the tokens above, so it names its
   own. They stay dark on every theme: this is a lightbox, and a pale scrim
   over screenshots reads as a broken overlay rather than as a dimmed room. */
.cv-media {
  --scrim: rgba(15, 13, 9, .9);
  --scrim-deep: rgba(15, 13, 9, .96);
  --scrim-ink: #f2efe6;
  --scrim-muted: #a9a49a;

  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--scrim);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--scrim-ink);
  font-family: var(--cv-font-body, system-ui, sans-serif);
  animation: cv-media-in .18s cubic-bezier(0, 0, .58, 1) both;
}

.cv-media[data-closing] { animation: cv-media-in .16s cubic-bezier(.42, 0, 1, 1) reverse forwards; }

@keyframes cv-media-in { from { opacity: 0; } to { opacity: 1; } }

.cv-media-dialog {
  width: min(680px, 100%);
  max-height: calc(100vh - 48px);
  display: flex;
  flex-direction: column;
  animation: cv-dialog-in .22s cubic-bezier(0, 0, .58, 1) both;
}

@keyframes cv-dialog-in { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }

.cv-media-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 0 4px 12px;
}

.cv-media-meta { flex: 1; min-width: 0; }
.cv-media-title { margin: 0; font-size: 1.05rem; font-weight: 600; letter-spacing: -.01em; line-height: 1.25; }

.cv-media-where {
  margin: 2px 0 0;
  font-family: var(--cv-font-mono, ui-monospace, monospace);
  font-size: .8rem;
  color: var(--scrim-muted);
}

.cv-media-where .date { white-space: nowrap; }
.cv-media-where .date.has-subtitle::before { content: "·"; margin: 0 6px; opacity: .6; }
.cv-media-where .to-sep { padding: 0 3px; }

.cv-media-close {
  appearance: none;
  flex: none;
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--scrim-muted);
  cursor: pointer;
  transition: color .2s ease, transform .2s ease;
}

.cv-media-close:hover,
.cv-media-close:focus-visible { color: var(--color-marker, #ffd23f); transform: scale(1.12); }

/* The grid changes shape with the count, so a single screenshot is not shown
   in a square crop and five are not shown as an awkward 3+2. */
.cv-media-grid { display: grid; gap: 10px; }
.cv-media-grid[data-count="1"] { grid-template-columns: 1fr; }
.cv-media-grid[data-count="1"] .cv-media-item { aspect-ratio: 16 / 9; }
.cv-media-grid[data-count="2"] { grid-template-columns: 1fr; }
.cv-media-grid[data-count="2"] .cv-media-item { aspect-ratio: 16 / 9; }
.cv-media-grid[data-count="3"] { grid-template-columns: 2fr 1fr; }
.cv-media-grid[data-count="3"] .cv-media-item:first-child { grid-row: span 2; }
.cv-media-grid[data-count="4"] { grid-template-columns: 1fr 1fr; }
.cv-media-grid[data-count="5"] { grid-template-columns: repeat(3, 1fr); }
.cv-media-grid[data-count="5"] .cv-media-item:first-child { grid-column: span 2; grid-row: span 2; }
.cv-media-grid[data-count="5"] .cv-media-item:nth-child(5) { grid-column: span 2; aspect-ratio: 2 / 1; }
.cv-media-grid[data-count="many"] { grid-template-columns: repeat(3, 1fr); }
.cv-media-grid[data-count="many"] .cv-media-item:first-child { grid-column: span 2; grid-row: span 2; }

.cv-media-item {
  margin: 0;
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  overflow: hidden;
  background: var(--scrim-deep);
  cursor: zoom-in;
  opacity: 0;
  transform: translateY(28px) scale(.92);
  animation: cv-item-in 420ms cubic-bezier(.16, 1, .3, 1) forwards;
  animation-delay: calc(var(--i) * 50ms + 30ms);
}

.cv-media-item:focus-visible { outline: 2px solid var(--scrim-ink); outline-offset: 3px; }

.cv-media-item img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  opacity: 0;
  transition: transform .4s ease, opacity .35s ease;
}

.cv-media-item img.is-loaded { opacity: 1; }
.cv-media-item:hover img { transform: scale(1.03); }

@keyframes cv-item-in { to { opacity: 1; transform: none; } }

.cv-media-footer { padding: 14px 4px 0; }

.cv-media-visit {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: .95rem;
  font-weight: 500;
  color: var(--scrim-ink);
  text-decoration: none;
}

.cv-media-visit .sweep { --cv-sweep: color-mix(in srgb, var(--color-marker, #ffd23f) 55%, transparent); }
.cv-media-visit:hover .sweep,
.cv-media-visit:focus-visible .sweep { background-size: 100% 100%; }
.cv-media-visit .arrow { font-size: .85em; opacity: .45; transition: opacity .2s ease, transform .2s ease; }
.cv-media-visit:hover .arrow { opacity: 1; transform: translate(2px, -2px); }

/* Expanded single image, layered over the grid. */
.cv-media-expanded {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--scrim-deep);
  cursor: zoom-out;
  animation: cv-media-in .18s cubic-bezier(0, 0, .58, 1) both;
}

.cv-media-expanded img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 8px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
  animation: cv-expanded-in 260ms cubic-bezier(.2, .8, .2, 1) both;
}

@keyframes cv-expanded-in { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }

@media (max-width: 680px) {
  .cv-media { padding: 12px; }
  .cv-media-grid[data-count] { grid-template-columns: repeat(2, 1fr); }
  .cv-media-grid[data-count] .cv-media-item:first-child { grid-column: span 2; grid-row: auto; aspect-ratio: 16 / 9; }
  .cv-media-title { font-size: 1rem; }
}

@media (prefers-reduced-motion: reduce) {
  .cv-media,
  .cv-media-dialog,
  .cv-media-item,
  .cv-media-expanded,
  .cv-media-expanded img { animation: none; opacity: 1; transform: none; }
  .cv-media-item:hover img { transform: none; }
}

@media print { .cv-media { display: none !important; } }
