/* ==========================================================================
   Katarína Grešová — personal site

   "Electric ink": crisp neutral paper, an indigo→violet accent, grotesk
   display type, and monospace for anything a machine would have printed —
   years, venues, tags, section numbers.

   One stylesheet, plain CSS. Sections in order:

     1. Design tokens (colours, type, spacing)  <- change things here first
     2. Reset & base
     3. Layout helpers
     4. Navigation
     5. Hero
     6. Sections & headings
     7. Publications
     8. Project cards
     9. Contact & footer
    10. Prose (for /page layout)
    11. CV timeline (for /cv/)
    12. Work list (for /building/ and /teaching/)
    13. By the numbers
    14. Upcoming strip
    15. Motion
    16. Responsive
   ========================================================================== */


/* 1. DESIGN TOKENS =========================================================

   Every colour is declared once, as light-dark(<light value>, <dark value>).
   The browser picks a side from the `color-scheme` below, which the two
   [data-theme] rules pin when a visitor uses the toggle. To restyle the site,
   you only ever edit this one block.
   ========================================================================== */

:root {
  color-scheme: light dark;

  /* Surfaces */
  --bg:       light-dark(#fafafb, #0b0b10);
  --bg-soft:  light-dark(#f2f1f7, #101018);
  --surface:  light-dark(#ffffff, #15151f);
  --bg-blur:  light-dark(rgba(250, 250, 251, .72), rgba(11, 11, 16, .72));

  /* Text & lines */
  --ink:      light-dark(#0b0b10, #eeedf5);
  --ink-soft: light-dark(#56526b, #9b97ae);
  --rule:     light-dark(#e4e3ec, #262633);

  /* Accent. --accent-2 is the far end of the gradient; both ends are dark
     enough in light mode (and light enough in dark mode) to carry text. */
  --accent:     light-dark(#5b4bff, #8b7bff);
  --accent-2:   light-dark(#9333ea, #c08cff);
  --accent-lit: light-dark(#4636d9, #a89cff);
  --on-accent:  light-dark(#ffffff, #0f0d1a);
  --wash:       light-dark(rgba(91, 75, 255, .06), rgba(139, 123, 255, .08));
  --ring:       light-dark(rgba(91, 75, 255, .30), rgba(139, 123, 255, .35));

  --gradient: linear-gradient(100deg, var(--accent), var(--accent-2));

  /* Hero atmospherics */
  --dot:    light-dark(#d5d2e4, #23232f);
  --glow-1: light-dark(rgba(91, 75, 255, .34), rgba(91, 75, 255, .46));
  --glow-2: light-dark(rgba(147, 51, 234, .26), rgba(192, 140, 255, .30));

  --shadow: 0 1px 2px light-dark(rgba(11, 11, 16, .04), rgba(0, 0, 0, .4)),
            0 14px 34px -18px light-dark(rgba(11, 11, 16, .22), rgba(0, 0, 0, .8));
  --shadow-lift: 0 2px 4px light-dark(rgba(11, 11, 16, .05), rgba(0, 0, 0, .4)),
                 0 24px 46px -22px light-dark(rgba(91, 75, 255, .45), rgba(0, 0, 0, .9));

  /* Type */
  --display: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
  --sans:    "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Metrics */
  --wrap:    62rem;   /* page gutter width */

  /* Reading width, defined as the page column itself: --wrap minus `.wrap`'s
     two 2.5rem gutters. A paragraph therefore ends exactly where the rules and
     the rows end, rather than stopping short and leaving half the page empty.

     The three values below move as one. Text this wide runs longer than the
     45–75 characters typographers like, so the page is kept narrower than it
     otherwise would be, the body size is stepped up, and the leading is
     generous — all three are what make a long line findable on the return
     sweep. Change one and the other two need re-checking. */
  --measure: calc(var(--wrap) - 5rem);

  --radius:    14px;
  --radius-lg: 22px;
  --ease:      cubic-bezier(.22, .68, .28, 1);
}

/* The toggle pins a side; without these the OS setting wins. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }


/* 2. RESET & BASE ========================================================= */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1.15rem;
  line-height: 1.75;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.15; }
p { margin: 0; }
img { display: block; max-width: 100%; height: auto; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a {
  color: inherit;
  text-decoration: none;
  transition: color .2s var(--ease);
}

::selection { background: var(--accent); color: var(--on-accent); }

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

.skip-link {
  position: absolute;
  left: 1rem;
  top: -4rem;
  z-index: 100;
  padding: .6rem 1rem;
  background: var(--ink);
  color: var(--bg);
  border-radius: 8px;
  font-size: .9rem;
  transition: top .2s var(--ease);
}
.skip-link:focus { top: 1rem; }

.icon {
  width: 1.05em;
  height: 1.05em;
  flex: none;
  vertical-align: -.16em;
}


/* 3. LAYOUT HELPERS ======================================================= */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 2.5rem);
}

/* Monospace label used above headings — the "machine printed this" voice. */
.eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .55rem;
  font-family: var(--mono);
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.eyebrow a { color: var(--ink); border-bottom: 1px solid var(--ring); }
.eyebrow a:hover { color: var(--accent); border-color: var(--accent); }
.eyebrow__sep { color: var(--rule); }

/* Live dot — a small sign that someone is actually here. */
.eyebrow__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: pulse 2.8s var(--ease) infinite;
}


/* 4. NAVIGATION =========================================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg-blur);
  backdrop-filter: saturate(1.8) blur(14px);
  -webkit-backdrop-filter: saturate(1.8) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color .3s var(--ease);
}
.nav--stuck { border-bottom-color: var(--rule); }

.nav__inner {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  height: 4.5rem;
}

.nav__mark {
  display: inline-flex;
  align-items: center;
  gap: .65rem;
  margin-right: auto;
}
.nav__badge {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 9px;
  background: var(--gradient);
  color: var(--on-accent);
  font-family: var(--mono);
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .04em;
  transition: transform .3s var(--ease);
}
.nav__mark:hover .nav__badge { transform: rotate(-6deg) scale(1.06); }
.nav__mark-name {
  font-family: var(--display);
  font-size: .98rem;
  font-weight: 600;
  letter-spacing: -.015em;
}

.nav__links {
  display: flex;
  gap: clamp(.9rem, 2.6vw, 1.9rem);
  font-family: var(--mono);
  font-size: .73rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.nav__links a {
  position: relative;
  padding-block: .35rem;
}
.nav__links a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: var(--gradient);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .3s var(--ease);
}
.nav__links a:hover { color: var(--ink); }
.nav__links a:hover::after,
.nav__links a[aria-current]::after { transform: scaleX(1); transform-origin: left; }
.nav__links a[aria-current] { color: var(--ink); }

.theme-toggle {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: transparent;
  color: var(--ink-soft);
  font-size: .95rem;
  cursor: pointer;
  transition: color .2s var(--ease), border-color .2s var(--ease), background .2s var(--ease);
}
.theme-toggle:hover { color: var(--accent); border-color: var(--ring); background: var(--wash); }
.theme-toggle > span { display: grid; place-items: center; }
.theme-toggle > .theme-toggle__moon { display: none; }
:root[data-theme="dark"] .theme-toggle > .theme-toggle__sun { display: none; }
:root[data-theme="dark"] .theme-toggle > .theme-toggle__moon { display: grid; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle > .theme-toggle__sun { display: none; }
  :root:not([data-theme="light"]) .theme-toggle > .theme-toggle__moon { display: grid; }
}

/* Reading-progress hairline along the bottom edge of the nav. */
.progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  overflow: hidden;
}
.progress span {
  display: block;
  height: 100%;
  background: var(--gradient);
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
}


/* 5. HERO ================================================================= */

.hero {
  position: relative;
  padding-block: clamp(1.75rem, 3.5vw, 2rem) clamp(1.75rem, 3vw, 2rem);
  overflow: hidden;
}

/* Faint dot grid. Both decorative layers below are masked with a `closest-side`
   ellipse so they reach every edge of their box already transparent — .hero
   clips to its own bounds, and anything still coloured there shows up as a
   hard rectangle. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, var(--dot) 1px, transparent 1.2px);
  background-size: 26px 26px;
  -webkit-mask-image: radial-gradient(closest-side ellipse at 50% 34%, #000 12%, transparent);
          mask-image: radial-gradient(closest-side ellipse at 50% 34%, #000 12%, transparent);
  pointer-events: none;
}

/* Slow-drifting colour wash behind the portrait. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0 -12% 0 26%;
  background:
    radial-gradient(closest-side ellipse at 40% 38%, var(--glow-1), transparent),
    radial-gradient(closest-side ellipse at 72% 64%, var(--glow-2), transparent);
  -webkit-mask-image: radial-gradient(closest-side ellipse at 50% 50%, #000 30%, transparent);
          mask-image: radial-gradient(closest-side ellipse at 50% 50%, #000 30%, transparent);
  pointer-events: none;
  animation: drift 26s var(--ease) infinite alternate;
}

.hero__inner {
  /* Shared by the name and the portrait, which is sized from it. */
  --name-size: clamp(2.5rem, 7.5vw, 4.5rem);

  position: relative;
  z-index: 1;
}

/* Top row: name on the left, portrait on the right, nothing else in it. The
   portrait is sized to the height of that whole block — two lines of the name
   plus the eyebrow above it — so its top edge lines up with the eyebrow and
   its bottom with the baseline of the name. Both terms come from --name-size,
   so the alignment survives the name scaling with the viewport. */
.hero__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: clamp(1.5rem, 5vw, 3.5rem);
}

.hero__name {
  margin-top: .85rem;
  font-family: var(--display);
  font-size: var(--name-size);
  font-weight: 600;
  line-height: .96;
  letter-spacing: -.045em;
}
.hero__name span { display: block; }

.hero__tagline {
  margin-top: 1.25rem;
  font-family: var(--display);
  font-size: clamp(1.35rem, 3.2vw, 2.1rem);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -.025em;
}
.mark { color: var(--accent); }
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .mark {
    background-image: var(--gradient);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

.hero__bio {
  margin-top: 1.4rem;
  max-width: var(--measure);
  line-height: 1.6;
  color: var(--ink-soft);
}
.hero__bio > * + * { margin-top: .85em; }


/* Square, and exactly as tall as the two lines of the name beside it: two
   lines at line-height .96 is 1.92 times the font size. Deriving it from
   --name-size rather than pinning a width keeps the two in step as the name
   scales with the viewport. The margin lines its top edge up with the name's,
   clearing the eyebrow above. */
.hero__portrait {
  position: relative;
  width: calc(var(--name-size) * 1.92 + 2.1rem);
}
.hero__portrait img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}
/* Gradient hairline ring, drawn as a masked border. */
.hero__portrait::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: calc(var(--radius-lg) + 1px);
  padding: 1.5px;
  background: var(--gradient);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}

/* A quiet line at the foot of the hero saying she is open to being contacted.
   Deliberately the smallest thing in the hero and the last one read: set in
   body type rather than the mono eyebrow, because the eyebrow reads as a label
   and a label here would look like a "FOR HIRE" badge. The dot is the same
   one as the eyebrow's, minus the pulse — it carries the status idiom without
   asking for attention. */
/* Flows as normal text rather than as a flex row: as a flex row the dot is an
   item of its own and wraps onto its own line on a phone. */
.hero__note {
  margin-top: 1.1rem;
  font-size: .92rem;
  color: var(--ink-soft);
}
.hero__note-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: .45rem;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: middle;
}
.hero__note a {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  color: var(--ink);
  border-bottom: 1px solid var(--ring);
}
.hero__note a:hover { color: var(--accent); border-bottom-color: var(--accent); }
.hero__note a:hover .icon--arrow { translate: 3px 0; }

/* The availability variant. The comment above still holds — a mono label here
   would read as a "FOR HIRE" badge — so this stays body type and gets its
   weight from a soft wash panel instead. Cornered with --radius rather than a
   pill, because the line wraps to two rows on a phone and a wrapped pill looks
   broken. It is the one fact a visitor needs in order to act, so it is no
   longer allowed to be the quietest thing in the hero. */
.hero__note--loud {
  display: inline-block;
  margin-top: 1.35rem;
  padding: .75rem 1.15rem;
  border: 1px solid var(--ring);
  border-radius: var(--radius);
  background: var(--wash);
  font-size: .95rem;
  color: var(--ink);
}
.hero__note--loud strong { font-weight: 600; }
.hero__note-where { color: var(--ink-soft); }
.hero__note--loud a { margin-left: .35rem; }

/* Icon link row, as chips. */
.links {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  margin-top: 1.25rem;
}
.links__item {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .45rem .95rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-soft);
  font-size: .86rem;
  font-weight: 500;
  transition: color .25s var(--ease), border-color .25s var(--ease),
              background .25s var(--ease), transform .25s var(--ease);
}
.links__item:hover {
  color: var(--accent);
  border-color: var(--ring);
  background: var(--wash);
  transform: translateY(-2px);
}


/* 6. SECTIONS ============================================================= */

.section { padding-block: clamp(3.5rem, 9vw, 6.5rem); }
.section--soft {
  background: var(--bg-soft);
  border-block: 1px solid var(--rule);
}

.section__title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .75rem 1.5rem;
  padding-bottom: 1.1rem;
  margin-bottom: 2.5rem;
  border-bottom: 1px solid var(--rule);
}
.section__label {
  display: flex;
  align-items: baseline;
  gap: .85rem;
  font-family: var(--display);
  font-size: clamp(1.45rem, 3.2vw, 2rem);
  font-weight: 600;
  letter-spacing: -.03em;
}
.section__num {
  font-family: var(--mono);
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .12em;
  color: var(--accent);
}
.section__aside {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-family: var(--mono);
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.section__aside:hover { color: var(--accent); }
.icon--arrow { transition: translate .25s var(--ease); }
.section__aside:hover .icon--arrow { translate: 3px 0; }

/* An optional line of prose between a section heading and its contents. */
.section__intro {
  max-width: var(--measure);
  margin-bottom: 2rem;
  color: var(--ink-soft);
}


/* 7. PUBLICATIONS ========================================================= */

.pubs { display: grid; }

.pub {
  position: relative;
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: 0 1.75rem;
  padding: 1.6rem 1.1rem;
  margin-inline: -1.1rem;
  border-radius: var(--radius);
  transition: background .3s var(--ease);
}
.pub:hover { background: var(--wash); }

/* Hairline between entries, inset so it lines up with the text, not the
   hover background. */
.pub::after {
  content: "";
  position: absolute;
  left: 1.1rem;
  right: 1.1rem;
  bottom: 0;
  height: 1px;
  background: var(--rule);
}
.pub:last-child::after { display: none; }

/* Accent bar that slides in on hover. */
.pub::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.6rem;
  bottom: 1.6rem;
  width: 3px;
  border-radius: 3px;
  background: var(--gradient);
  opacity: 0;
  transition: opacity .3s var(--ease);
}
.pub:hover::before { opacity: 1; }

.pub__year {
  font-family: var(--mono);
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: .06em;
  color: var(--ink-soft);
  padding-top: .35rem;
  transition: color .3s var(--ease);
}
.pub:hover .pub__year { color: var(--accent); }

.pub__title {
  font-family: var(--display);
  font-size: clamp(1.1rem, 2.1vw, 1.35rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -.025em;
}
.pub__title a {
  background-image: var(--gradient);
  background-repeat: no-repeat;
  background-size: 0% 2px;
  background-position: 0 100%;
  padding-bottom: .08em;
  transition: background-size .35s var(--ease);
}
.pub__title a:hover { background-size: 100% 2px; }

.pub__authors {
  margin-top: .6rem;
  font-size: .89rem;
  color: var(--ink-soft);
}
.pub__authors strong { color: var(--ink); font-weight: 600; }

.pub__venue {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .65rem;
  margin-top: .7rem;
  font-size: .84rem;
  color: var(--ink-soft);
}
.pub__journal {
  padding: .18rem .55rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--surface);
  font-family: var(--mono);
  font-size: .66rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink);
}


/* Papers she led. Deliberately the loudest chip in the row: first authorship
   is the first thing anyone scanning a publication list looks for, and it is
   invisible otherwise unless you read every author list. */
.pub__first {
  padding: .18rem .5rem;
  border-radius: 999px;
  background: var(--gradient);
  color: var(--on-accent);
  font-family: var(--mono);
  font-size: .64rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* Small qualifier after the venue — "Preprint", "Review". */
.pub__note {
  padding: .18rem .5rem;
  border-radius: 999px;
  background: var(--wash);
  color: var(--accent);
  font-family: var(--mono);
  font-size: .64rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
}


/* 8. PROJECT CARDS ======================================================== */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1.25rem;
}

.card {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  transition: transform .35s var(--ease), border-color .35s var(--ease),
              box-shadow .35s var(--ease);
}
/* Gradient rule that wipes across the top edge on hover. */
.card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: var(--gradient);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .4s var(--ease);
}
.card:hover {
  transform: translateY(-4px);
  border-color: var(--ring);
  box-shadow: var(--shadow-lift);
}
.card:hover::before { transform: scaleX(1); }

.card__link {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 1.6rem;
}

/* Optional headline image, set with `image:` on an entry in _data/work.yml.
   It sits inside the padded link so the whole card stays one click target, and
   bleeds back out to the card's edges with a negative margin. Every file is
   pre-rendered to the same 900x360, so a row of cards never goes ragged, and
   the white backing keeps a figure drawn on white legible in dark mode. */
.card__media {
  display: block;
  margin: -1.6rem -1.6rem 1.3rem;
  padding: .9rem 1.1rem;
  border-bottom: 1px solid var(--rule);
  background: #fff;
}
/* The frame is a fixed 5:2 so a row of cards stays even, but the picture is
   letterboxed inside it rather than cropped: these are figures and logos, and
   cropping one cuts off the part that carried the meaning. Whatever shape the
   file is, it lands whole, with white to the sides or above and below. */
.card__media img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 2;
  object-fit: contain;
}

.card__title {
  display: flex;
  align-items: center;
  gap: .45rem;
  font-family: var(--display);
  font-size: 1.2rem;
  font-weight: 600;
  letter-spacing: -.025em;
}
.card__title .icon--arrow {
  color: var(--accent);
  opacity: 0;
  translate: -4px 0;
  transition: opacity .25s var(--ease), translate .25s var(--ease);
}
.card:hover .card__title .icon--arrow { opacity: 1; translate: 0 0; }

.card__blurb {
  margin-top: .7rem;
  font-size: .92rem;
  color: var(--ink-soft);
}

.card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-top: auto;
  padding-top: 1.4rem;
}
.card__tags span {
  padding: .2rem .6rem;
  border-radius: 999px;
  background: var(--wash);
  color: var(--accent);
  font-family: var(--mono);
  font-size: .66rem;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
}


/* 9. CONTACT & FOOTER ===================================================== */

.contact { padding-bottom: clamp(4rem, 10vw, 7rem); }

.contact__inner {
  position: relative;
  overflow: hidden;
  padding: clamp(2.25rem, 6vw, 4rem);
  border: 1px solid var(--rule);
  border-radius: calc(var(--radius-lg) + 6px);
  background: var(--surface);
  box-shadow: var(--shadow);
}
/* Corner wash. Sized in % so it stays a corner accent on a phone instead of
   swallowing the whole card, and masked like the hero washes. */
.contact__inner::before {
  content: "";
  position: absolute;
  top: -52%;
  right: -16%;
  width: min(34rem, 78%);
  aspect-ratio: 1;
  background:
    radial-gradient(closest-side ellipse at 42% 58%, var(--glow-1), transparent),
    radial-gradient(closest-side ellipse at 62% 42%, var(--glow-2), transparent);
  -webkit-mask-image: radial-gradient(closest-side ellipse at 50% 50%, #000 8%, transparent);
          mask-image: radial-gradient(closest-side ellipse at 50% 50%, #000 8%, transparent);
  pointer-events: none;
}
.contact__body,
.contact__title,
.contact .button { position: relative; }

.contact__title {
  font-family: var(--display);
  font-size: clamp(1.8rem, 4.6vw, 2.8rem);
  font-weight: 600;
  letter-spacing: -.035em;
}

.contact__body {
  margin-top: 1rem;
  max-width: var(--measure);
  color: var(--ink-soft);
}

.button {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  margin-top: 2rem;
  padding: .85rem 1.5rem;
  border-radius: 999px;
  background: var(--gradient);
  color: var(--on-accent);
  font-size: .93rem;
  font-weight: 500;
  box-shadow: 0 12px 30px -14px var(--accent);
  transition: transform .25s var(--ease), box-shadow .25s var(--ease);
}
.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 18px 38px -14px var(--accent);
}
.button:hover .icon--arrow { translate: 3px 0; }

/* Outlined variant, for a button that is useful but is not the thing the page
   is asking you to do — the CV download sitting under a page header, where the
   filled gradient version would outrank the header itself. */
.button--quiet {
  margin-top: 0;
  padding: .6rem 1.15rem;
  border: 1px solid var(--rule);
  background: none;
  color: var(--ink);
  font-size: .86rem;
  box-shadow: none;
}
.button--quiet:hover {
  border-color: var(--ring);
  background: var(--wash);
  color: var(--accent);
  box-shadow: none;
}

.foot {
  border-top: 1px solid var(--rule);
  padding-block: 2.5rem;
  font-size: .82rem;
  color: var(--ink-soft);
}
.foot__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: .5rem 1.5rem;
}
.foot__name {
  font-family: var(--display);
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--ink);
}
.foot__meta { font-family: var(--mono); font-size: .72rem; letter-spacing: .04em; }
.foot a { border-bottom: 1px solid var(--rule); }
.foot a:hover { color: var(--accent); border-color: var(--accent); }
.foot__sep { margin-inline: .5rem; color: var(--rule); }

.foot__year { margin-left: .5rem; }


/* 10. PROSE (generic /page layout) ======================================== */

.page { padding-block: clamp(3rem, 8vw, 5rem); }
.page__title {
  margin-top: .85rem;
  font-family: var(--display);
  font-size: clamp(2.2rem, 6vw, 3.4rem);
  font-weight: 600;
  letter-spacing: -.04em;
}
.page__subtitle {
  margin-top: .9rem;
  max-width: var(--measure);
  font-size: 1.05rem;
  color: var(--ink-soft);
}
.page__head {
  padding-bottom: 2rem;
  margin-bottom: 2.5rem;
  border-bottom: 1px solid var(--rule);
}

.prose { max-width: var(--measure); }
.prose > * + * { margin-top: 1.1em; }
.prose h2 {
  font-family: var(--display);
  font-size: 1.6rem;
  letter-spacing: -.03em;
  margin-top: 2.25em;
}
.prose h3 { font-family: var(--display); font-size: 1.25rem; letter-spacing: -.02em; margin-top: 1.8em; }
.prose a {
  color: var(--accent);
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: 0 100%;
}
.prose a:hover { color: var(--accent-lit); }
.prose ul { list-style: disc; padding-left: 1.2em; }
.prose li + li { margin-top: .35em; }
.prose code {
  padding: .12em .38em;
  border-radius: 5px;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: .85em;
}
.prose blockquote {
  margin-inline: 0;
  padding-left: 1.1rem;
  border-left: 3px solid var(--accent);
  color: var(--ink-soft);
}


/* Prose-only pages (the `page` layout: /research/ and anything like it).
   Without this the text column sits against the left edge of a 70rem wrap and
   the page reads as broken rather than narrow. The 5rem is twice `.wrap`'s
   maximum padding, so the text column ends up exactly one --measure wide. */
.page--prose { max-width: calc(var(--measure) + 5rem); }
.page--prose .prose { max-width: none; }


/* 11. CV TIMELINE (for /cv/) ============================================= */

/* Same left rail as the publication list, quieter: the CV is scanned, not
   read, so nothing here lifts or glows on hover. */

.cv__section + .cv__section { margin-top: 3.25rem; }

.cv__heading {
  font-family: var(--mono);
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding-bottom: .9rem;
  margin-bottom: .4rem;
  border-bottom: 1px solid var(--rule);
}

.cv { display: grid; }

.cv__item {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 0 1.75rem;
  padding-block: 1.25rem;
}
.cv__item + .cv__item { border-top: 1px solid var(--rule); }

.cv__when {
  font-family: var(--mono);
  font-size: .78rem;
  letter-spacing: .04em;
  color: var(--ink-soft);
  padding-top: .2rem;
}

.cv__role {
  font-family: var(--display);
  font-size: 1.08rem;
  font-weight: 600;
  letter-spacing: -.02em;
}
.cv__role a { color: var(--accent); }
.cv__role a:hover { color: var(--accent-lit); }

.cv__where  { margin-top: .3rem; font-size: .9rem; color: var(--ink-soft); }
.cv__detail { margin-top: .5rem; font-size: .9rem; max-width: var(--measure); }

.cv__note {
  margin-top: 1rem;
  font-family: var(--mono);
  font-size: .74rem;
  letter-spacing: .04em;
}

.cv__list {
  list-style: disc;
  padding-left: 1.2em;
  margin-top: 1.1rem;
  max-width: var(--measure);
}
.cv__list li + li { margin-top: .4em; }


/* 12. WORK LIST (for /building/ and /teaching/) =========================== */

/* The same left rail as the CV, but each row carries a row of links, so it
   gets more vertical room and the name is set a size larger. */

.work__group + .work__group { margin-top: 3.5rem; }

/* On /teaching/ an intro paragraph sits between the page header and the first
   group, so the first heading gets no air from the rule above it. Same gap. */
.prose + .work__group { margin-top: 3.5rem; }

.work__intro {
  margin-top: 1rem;
  max-width: var(--measure);
  font-size: .95rem;
  color: var(--ink-soft);
}

.work { display: grid; margin-top: .5rem; }

.work__item {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 0 1.75rem;
  padding-block: 1.6rem;
}
.work__item + .work__item { border-top: 1px solid var(--rule); }

.work__when {
  font-family: var(--mono);
  font-size: .78rem;
  letter-spacing: .04em;
  color: var(--ink-soft);
  padding-top: .3rem;
}

.work__name {
  font-family: var(--display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -.025em;
}

.work__where {
  margin-top: .25rem;
  font-family: var(--mono);
  font-size: .74rem;
  letter-spacing: .04em;
  color: var(--ink-soft);
}

.work__blurb {
  margin-top: .6rem;
  max-width: var(--measure);
  color: var(--ink-soft);
}

/* Links read as a row of small chips. The arrow only appears on hover, the
   same tell the project cards use. */
.work__links {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: .9rem;
}
.work__links a {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .22rem .65rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: .7rem;
  letter-spacing: .04em;
  color: var(--ink-soft);
  transition: border-color .25s var(--ease), color .25s var(--ease);
}
.work__links a:hover { border-color: var(--ring); color: var(--accent); }
.work__links .icon--arrow {
  opacity: 0;
  translate: -3px 0;
  transition: opacity .25s var(--ease), translate .25s var(--ease);
}
.work__links a:hover .icon--arrow { opacity: 1; translate: 0 0; }

.work__tags {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-top: .8rem;
}
.work__tags span {
  font-family: var(--mono);
  font-size: .66rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  opacity: .75;
}
.work__tags span + span::before { content: "\00b7"; margin-right: .4rem; }


/* 13. BY THE NUMBERS ===================================================== */

/* A band of counts under the hero. The numbers borrow .mark from the hero for
   the gradient fill, so they stay in step with the tagline. Set in a <dl>:
   each figure is a term and its explanation the description. */

.stats {
  border-block: 1px solid var(--rule);
  background: var(--bg-soft);
}

.stats__inner {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 2rem 1.5rem;
  padding-block: clamp(2.25rem, 5vw, 3rem);
}

.stat__num {
  font-family: var(--display);
  font-size: clamp(2.4rem, 5.5vw, 3.4rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -.04em;
}

.stat__label {
  margin: .7rem 0 0;
  font-family: var(--mono);
  font-size: .7rem;
  font-weight: 500;
  line-height: 1.65;
  letter-spacing: .04em;
  color: var(--ink-soft);
}


/* 14. UPCOMING STRIP ====================================================== */

/* A slim band under the hero listing the next few places to find her. It
   renders only while _data/upcoming.yml holds an event that has not happened
   yet, so the band disappears on its own rather than going stale. */

.upcoming {
  border-block: 1px solid var(--rule);
  background: var(--wash);
}

.upcoming__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .75rem 2.25rem;
  padding-block: 1.15rem;
}

.upcoming__label {
  font-family: var(--mono);
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent);
}

/* One event per row, with the dates sharing a column so the titles line up.
   The alignment needs `display: contents` on the <li> below: that makes each
   event's two spans direct children of this grid, which is the only way one
   row can size a track the next row also uses. */
.upcoming__list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .55rem 1.25rem;
  min-width: 0;
}

.upcoming__event { display: contents; }

.upcoming__when {
  font-family: var(--mono);
  font-size: .74rem;
  letter-spacing: .04em;
  color: var(--ink-soft);
  white-space: nowrap;
}

.upcoming__what { font-size: .92rem; }
.upcoming__where { color: var(--ink-soft); }

/* Same hairline underline as the eyebrow links, so an event that has a page
   behind it looks clickable without shouting. */
.upcoming__what a { border-bottom: 1px solid var(--ring); }
.upcoming__what a:hover { color: var(--accent); border-bottom-color: var(--accent); }


/* 15. OFFER CARDS ========================================================= */

/* The three doors under the hero, and the only things on the page a visitor is
   actually asked to act on. They are deliberately louder than the project
   cards further down: the gradient rule along the top edge is always lit
   rather than waiting for a hover, and they carry more padding than anything
   else on the page.

   Laid out as a flex column so the proof line can be pushed to the foot with
   `margin-top:auto`. The three cards hold different amounts of detail, and
   without that the three proof lines sit at three different heights. */

.offers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: 1.25rem;
  margin-top: 2.25rem;
}

.offer__card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 1.9rem 1.7rem 1.5rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  transition: transform .35s var(--ease), border-color .35s var(--ease),
              box-shadow .35s var(--ease);
}
.offer__card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: var(--gradient);
}
.offer__card:hover {
  transform: translateY(-4px);
  border-color: var(--ring);
  box-shadow: var(--shadow-lift);
}

.offer__name {
  margin: 0;
  font-family: var(--display);
  font-size: 1.22rem;
  font-weight: 600;
  letter-spacing: -.02em;
}

/* The promise, and the only line in the card some people will read. Set in the
   accent so it separates from the detail underneath at a glance. */
.offer__lede {
  margin: .55rem 0 0;
  font-size: .98rem;
  font-weight: 500;
  line-height: 1.5;
  color: var(--accent);
}

.offer__detail {
  margin: .85rem 0 1.35rem;
  font-size: .9rem;
  line-height: 1.7;
  color: var(--ink-soft);
}

/* A prior instance rather than a promise, pinned to the foot above a hairline
   so the card ends on evidence. */
.offer__proof {
  margin: auto 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-size: .8rem;
  line-height: 1.6;
  color: var(--ink-soft);
}
.offer__proof a {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  margin-left: .15rem;
  color: var(--accent);
  font-weight: 500;
  white-space: nowrap;
}
.offer__proof a:hover { color: var(--accent-lit); }
.offer__proof a:hover .icon--arrow { translate: 3px 0; }

/* The date stamp under the numbers strip. Two of the four figures are read off
   Scholar and GitHub by hand, so the page says when — an uncited count of
   citations is exactly the sort of number this site exists to complain about. */
.stats__asof {
  margin: 0;
  padding-bottom: clamp(1.25rem, 3vw, 1.75rem);
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .04em;
  color: var(--ink-soft);
  opacity: .75;
}

.contact__where {
  margin: 1.1rem 0 0;
  font-size: .88rem;
  color: var(--ink-soft);
}

/* 16. MOTION ============================================================== */

/* Hero: pure CSS, runs at first paint whether or not JS is alive. */
.hero .reveal {
  animation: rise .8s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 80ms + 40ms);
}
@keyframes rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--ring); }
  50%      { box-shadow: 0 0 0 5px transparent; }
}

@keyframes drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-4%, 3%, 0); }
}

/* Below the fold: faded until scrolled to. `.js` is added by the inline
   script in <head>, so with JavaScript off the observer never runs and
   everything stays visible. */
.js .section .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .75s var(--ease), transform .75s var(--ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.js .section .reveal.is-visible { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero .reveal { animation: none; }
  .hero::after { animation: none; }
  .eyebrow__dot { animation: none; }
  .js .section .reveal { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}


/* 17. RESPONSIVE ========================================================== */

@media (max-width: 52rem) {
  .nav__mark-name { display: none; }
}

@media (max-width: 46rem) {
  /* Four figures in a row stop reading as a row long before the text does. */
  .stats__inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .hero__head { grid-template-columns: minmax(0, 1fr); }
  .hero__portrait {
    order: -1;
    width: clamp(7.5rem, 34vw, 10rem);
  }
  .pub {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  .pub__year { padding-top: 0; margin-bottom: .45rem; }
  .cv__item {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  .cv__when { padding-top: 0; margin-bottom: .4rem; }
  .work__item {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  .work__when { padding-top: 0; margin-bottom: .4rem; }
  .upcoming__inner { flex-direction: column; gap: .7rem; }
  /* The date column crowds the title on a phone, so each event goes back to
     being one flex row that can wrap onto a second line. */
  .upcoming__list { grid-template-columns: minmax(0, 1fr); gap: .9rem; }
  .upcoming__event {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .6rem;
    min-width: 0;
  }
  .nav__links { gap: 1rem; font-size: .7rem; letter-spacing: .08em; }
}

/* Five nav items stop fitting on one line somewhere around a phone's width,
   so below this the bar becomes two rows: mark and theme toggle on top, the
   links spread across the row beneath. */
@media (max-width: 34rem) {
  .nav__inner {
    flex-wrap: wrap;
    height: auto;
    gap: 0 1rem;
    padding-block: .7rem;
  }
  .nav__links {
    order: 3;
    width: 100%;
    justify-content: space-between;
    gap: .5rem;
    padding-top: .6rem;
    font-size: .66rem;
    letter-spacing: .06em;
  }
}

@media (max-width: 26rem) {
  .nav__links { font-size: .62rem; letter-spacing: .04em; }
  .links { gap: .45rem; }
  .links__item { padding: .4rem .75rem; font-size: .8rem; }
}


/* 18. PRINT =============================================================== */

/* Name and contact details for the printed CV only. */
.print-id { display: none; }

/* Exists so /cv/ can be printed to the PDF that lives at
   /assets/katarina-gresova-cv.pdf — the HTML CV stays the single source of
   truth and the PDF is re-rendered from it rather than maintained by hand.
   (See the note in README.md for the one command that regenerates it.)

   Everything that only makes sense on screen comes out: the bar, the theme
   toggle, the scroll progress hairline, the footer, and the reveal animation,
   which would otherwise print the whole page at opacity 0. */

@media print {
  .nav,
  .progress,
  .theme-toggle,
  .button,
  .foot { display: none !important; }

  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  :root { color-scheme: light; }

  body {
    background: #fff;
    color: #000;
    font-size: 10.5pt;
  }

  .wrap { max-width: none; padding-inline: 0; }
  .page { padding-block: 0; }

  a { color: #000; text-decoration: none; }

  .print-id {
    display: block;
    margin-bottom: 1.6rem;
    padding-bottom: .9rem;
    border-bottom: 1px solid #ccc;
  }
  .print-id__name {
    margin: 0;
    font-family: var(--display);
    font-size: 15pt;
    font-weight: 600;
    letter-spacing: -.02em;
  }
  .print-id__meta {
    margin: .3rem 0 0;
    font-family: var(--mono);
    font-size: 8pt;
    color: #444;
  }

  /* A role and its detail splitting across two sheets is the one break that
     actually costs the reader something. */
  .cv__item,
  .work__item,
  .pub { break-inside: avoid; }

  .cv__section,
  .work__group { break-before: auto; }

  .cv__heading { break-after: avoid; }
}
