/* ken liivik — portfolio
   Design values measured from the Framer original at a 1425px content viewport.
   Fit-to-width headings are sized as a ratio of 100cqw (the .wrap container's
   content width), so each fills the measure exactly at every screen size. */

:root {
  --bg:        #283618;
  --fg:        #fefae0;
  /* The original had a third, slightly warmer white for the nav. The palette
     is two colours now, so the nav uses --fg; restore a variant here if the
     nav ever wants to sit apart again. */
  --fg-nav:    #fefae0;
  /* --fg at 12%. Written out rather than derived with color-mix() so it still
     renders on browsers predating relative colour syntax. Update alongside
     --fg. */
  --rule:      rgba(254, 250, 224, 0.12);
  --pad:       28px;
  --gap:       24px;

  /* Fit-to-width headings, measured from canvas ink extents (not the layout
     box) for Funnel Sans 700 at -0.06em tracking.

     A text box is not its glyphs. The first letter's ink sits inside the box
     by its left side bearing, and the negative tracking pulls the box's right
     edge in while the last glyph's ink still reaches its natural width. For
     "ken liivik" at font-size 100 that is 6.27px of gap on the left and
     5.17px of overhang on the right — which reads as the heading starting
     indented and running long.

     --k-*   font-size / INK width, so the visible letters span the measure.
     --lsb-* left side bearing / ink width; the heading is pulled left by this
             much so its first glyph lands on the content edge.
     Re-measure both if the font or its tracking changes. */
  --k-wordmark: 0.291773;   /* "ken liivik"    */
  --k-work:     0.177515;   /* "selected work" */
  --k-about:    0.262422;   /* "about me"      */

  --k-blog:     0.352651;   /* "writing"       */

  --lsb-wordmark: 0.018285;
  --lsb-work:     0.007101;
  --lsb-about:    0.008399;
  --lsb-blog:     0.005290;

  /* Word-by-word scroll fill — see "scroll-linked brightening" below.
     Measured at 1440x900, where the statement wraps to 5 lines (395px) and the
     cover range spans 1295px. That maps to the block's travel as:
        0%      top edge just touching the bottom of the viewport
       30.5%    block fully on screen
       69.5%    top edge reaching the top — it starts leaving here
     Start before ~30% and words light while the text is still coming in from
     the bottom; finish after ~69% and the closing words land off-screen.
     Start and step trade against each other: a later start leaves less room,
     so the step must shrink to keep the ending under the ceiling.
     Narrow viewports wrap the statement taller, which squeezes this window —
     the values target desktop, where the effect actually reads. */
  --fill-start:  35%;   /* where the first word begins brightening */
  --fill-window: 3%;    /* one word's own fade — smaller = harder snap */
  --fill-step:   1%;    /* gap between words — bigger = slower sweep */

  /* Whole-block fill, used by the about-page bio paragraphs. Those are short
     (60-95px), so their cover range is mostly viewport travel and a wide
     window turns into a very long, smooth drift — 40% of the range worked out
     to 285px of scrolling. 10% lands it in ~70px instead. */
  --fill-block-start:  38%;
  --fill-block-window: 10%;
}

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

/* Any author `display` beats the UA stylesheet's [hidden] { display: none },
   so setting el.hidden silently does nothing on styled elements. This makes
   the attribute mean what it says everywhere. */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: 'Funnel Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-weight: 500;
  font-size: 20px;
  line-height: 1.2;
  letter-spacing: -0.04em;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
a:hover { opacity: 0.7; }

/* Declaring .wrap a size container makes 100cqw equal its real content width.
   100vw can't be used here — it includes the scrollbar, which made the
   fit-to-width headings overshoot by exactly the scrollbar's width. */
.wrap {
  padding-inline: var(--pad);
  container-type: inline-size;
}

/* ---------- nav ---------- */

/* The display headings use line-height 0.9, so their glyph boxes overflow
   above the element box and were swallowing clicks on the nav. container-type
   makes each .wrap a stacking context, so <main> painted over <header> in DOM
   order — lifting the header restores the links. */
.site-header { position: relative; z-index: 2; }

.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-block: 12px;
  color: var(--fg-nav);
  font-size: 20px;
  font-weight: 500;
}
.nav a { color: var(--fg-nav); }

/* ---------- fit-to-width display type ---------- */

.display {
  margin: 0;
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.06em;
  white-space: nowrap;

  /* line-height 0.9 means the glyph area is taller than the line box, so at
     these sizes the ink overflows well past the element and swallows clicks
     on whatever sits above or below it — the footer's "work" link at wide
     viewports, the nav before that. These headings contain no links and
     nothing needs to click them, so take them out of hit testing entirely.
     Screen readers are unaffected; only pointer targeting changes. */
  pointer-events: none;
}
.display--wordmark {
  font-size: calc(100cqw * var(--k-wordmark));
  margin-left: calc(-100cqw * var(--lsb-wordmark));
}
.display--work {
  font-size: calc(100cqw * var(--k-work));
  margin-left: calc(-100cqw * var(--lsb-work));
}
.display--about {
  font-size: calc(100cqw * var(--k-about));
  margin-left: calc(-100cqw * var(--lsb-about));
}
.display--blog {
  font-size: calc(100cqw * var(--k-blog));
  margin-left: calc(-100cqw * var(--lsb-blog));
}

/* ---------- handwritten wordmark ---------- */

/* Replaces the set "ken liivik" in the hero and footer. An SVG fills its
   container exactly by its own geometry, so none of the --k/--lsb fit-to-width
   machinery is needed here — those still drive the other display headings.
   Referenced rather than inlined: the file is 10KB and appears on every page,
   which would more than double the HTML. The colour is baked into the file
   because <img> cannot inherit currentColor. */
.wordmark {
  display: block;
  width: 100%;
  height: auto;
}
.hero__mark { margin: 0; }

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

.hero { padding-top: 52px; }
.hero__tagline {
  margin: 28px 0 0 auto;
  max-width: 600px;
  font-size: 24px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.04em;
}

/* ---------- project grid ---------- */

.grid {
  display: grid;
  gap: var(--gap);
  margin-top: 96px;
}
.grid--even    { grid-template-columns: 1fr 1fr; }
.grid--feature { grid-template-columns: 847fr 498fr; }
.grid--thirds  { grid-template-columns: repeat(3, 1fr); gap: 20px; }

.card__media {
  width: 100%;
  height: auto;
  object-fit: cover;
  aspect-ratio: var(--ar, 1.072);
}
.card__title {
  margin: 12px 0 0;
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -0.04em;
}
.card__meta {
  margin: 4px 0 0;
  font-size: 16px;
  font-weight: 400;
  letter-spacing: -0.02em;
}

/* the home grid staggers: each cell keeps its own aspect ratio */
.grid--stagger { align-items: start; }

/* ---------- work page ---------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  margin-top: 48px;
  font-size: 24px;
  font-weight: 500;
}
.filters button {
  all: unset;
  cursor: pointer;
  letter-spacing: -0.04em;
}
.filters button[aria-pressed="true"] { text-decoration: underline; text-underline-offset: 4px; }
.filters button:hover { opacity: 0.7; }

.loadmore {
  display: block;
  margin: 72px auto 0;
  padding: 10px 22px;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 16px;
  cursor: pointer;
}
.loadmore:hover { opacity: 0.7; }

/* ---------- long-form statement ---------- */

.statement {
  margin: 120px 0 0;
  font-size: 72px;
  font-weight: 500;
  line-height: 1.0972;
  letter-spacing: -0.04em;
}

/* ---------- three-column strip ---------- */

.strip {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 40px;
  margin-top: 96px;
  align-items: start;
}
.strip__label {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 400;
  letter-spacing: -0.02em;
}
.strip__body {
  margin: 0;
  font-size: 20px;
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: -0.02em;
}
.strip__portrait { aspect-ratio: 0.9247; object-fit: cover; width: 100%; }

/* ---------- project detail ---------- */

.project__title { margin: 80px 0 0; font-size: 120px; font-weight: 700; line-height: 1; letter-spacing: -0.06em; text-align: center; }
.project__hero  { margin-top: 84px; width: 100%; aspect-ratio: 1.6818; object-fit: cover; }

.label { margin: 0; font-size: 20px; font-weight: 400; letter-spacing: -0.02em; opacity: 0.7; }

.project__overview {
  margin: 34px 0 0;
  font-size: 44px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

.meta {
  display: flex;
  justify-content: space-between;
  gap: 32px;
  margin-top: 90px;
}
.meta__value { margin: 8px 0 0; font-size: 24px; font-weight: 500; letter-spacing: -0.04em; }

.gallery { margin-top: 96px; display: grid; gap: var(--gap); }
.gallery__full { width: 100%; aspect-ratio: 1.6818; object-fit: cover; }
.gallery__pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); }
.gallery__pair img { width: 100%; aspect-ratio: 1.2942; object-fit: cover; }

.section-title { margin: 120px 0 0; font-size: 120px; font-weight: 700; line-height: 1; letter-spacing: -0.06em; }

/* ---------- blog ---------- */

/* Post list: same rule treatment as the experience rows, so the two index-like
   pages feel related. */
.posts { margin-top: 84px; border-bottom: 1px solid var(--rule); }

.post-link {
  display: grid;
  grid-template-columns: 220fr 520fr 629fr;
  align-items: baseline;
  padding-block: 26px;
  border-top: 1px solid var(--rule);
}
.post-link__date    { font-size: 16px; font-weight: 400; letter-spacing: -0.02em; opacity: 0.7; }
.post-link__title   { font-size: 24px; font-weight: 500; letter-spacing: -0.04em; }
.post-link__excerpt { font-size: 16px; font-weight: 400; letter-spacing: -0.02em; opacity: 0.7; }

/* Single post. Measure is capped for readability — full content width at
   this size would run well past a comfortable line length. */
.article { margin-top: 80px; }
.article__date  { margin-bottom: 18px; }
.article__title {
  margin: 0;
  max-width: 20ch;
  font-size: 96px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.05em;
}

/* Prose is rendered HTML from WordPress, so these style whatever the editor
   produces rather than classes of our own. */
.prose {
  max-width: 62ch;
  margin-top: 64px;
  font-size: 22px;
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: -0.01em;
}
.prose > * { margin: 0 0 1.2em; }
.prose > *:last-child { margin-bottom: 0; }
.prose h2 {
  margin: 2em 0 0.6em;
  font-size: 34px;
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.03em;
}
.prose h3 {
  margin: 1.8em 0 0.5em;
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -0.03em;
}
.prose a { text-decoration: underline; text-underline-offset: 3px; }
.prose strong { font-weight: 500; }
.prose ul, .prose ol { padding-left: 1.2em; }
.prose li { margin-bottom: 0.4em; }
.prose blockquote {
  margin: 1.6em 0;
  padding-left: 24px;
  border-left: 1px solid var(--rule);
  font-size: 26px;
  letter-spacing: -0.02em;
}
.prose img, .prose figure { margin: 2em 0; max-width: 100%; height: auto; }
.prose figcaption { margin-top: 10px; font-size: 16px; opacity: 0.7; }
.prose code {
  padding: 2px 6px;
  border-radius: 3px;
  background: var(--rule);
  font-size: 0.85em;
}
.prose pre { padding: 20px; overflow-x: auto; background: var(--rule); border-radius: 4px; }
.prose pre code { padding: 0; background: none; }
.prose hr { border: 0; border-top: 1px solid var(--rule); margin: 2.4em 0; }

/* ---------- footer ---------- */

.footer { margin-top: 140px; }
.footer__top { display: flex; justify-content: space-between; gap: 40px; flex-wrap: wrap; }
.footer__contact p { margin: 0 0 8px; font-size: 20px; font-weight: 400; letter-spacing: -0.02em; }
.footer__cols { display: flex; gap: 40px; }
.footer__col h2 { margin: 0 0 9px; font-size: 16px; font-weight: 400; letter-spacing: -0.02em; opacity: 0.7; }
.footer__col ul { margin: 0; padding: 0; list-style: none; }
.footer__col li { margin-bottom: 10px; font-size: 20px; font-weight: 500; }
.footer__wordmark { margin-top: 48px; }
/* 1px rule across the full content width, between the wordmark and the
   copyright — same colour as the experience-row rules. */
.footer__legal {
  margin: 30px 0 0;
  padding-top: 16px;
  padding-bottom: 40px;
  border-top: 1px solid var(--rule);
  font-size: 16px;
  font-weight: 400;
  letter-spacing: -0.02em;
  text-transform: lowercase;
}

/* ---------- experience list ---------- */

/* Rows are separated by 1px rules spanning the full content width: one above
   each row plus a closing one, matching the original's nine lines for eight
   rows. Row pitch measures 97px. */
.exp { margin-top: 84px; border-bottom: 1px solid var(--rule); text-transform: lowercase; }
/* measured columns: 371 / 360 / 371 with no gaps, spanning the content width.
   The date column is right-aligned so it meets the right edge of the rules. */
.exp__row {
  display: grid;
  grid-template-columns: 371fr 360fr 371fr;
  padding-block: 19px;
  align-items: center;
  border-top: 1px solid var(--rule);
}
.exp__date { text-align: right; }
.exp__role    { font-size: 24px; font-weight: 500; letter-spacing: -0.04em; }
.exp__company { margin: 6px 0 0; font-size: 20px; font-weight: 400; opacity: 0.7; }
.exp__note    { font-size: 16px; font-weight: 400; letter-spacing: -0.02em; opacity: 0.7; }
.exp__date    { font-size: 24px; font-weight: 500; letter-spacing: -0.04em; }

/* ---------- about ---------- */

/* measured: text column 694px, gap 64px, portrait 611px = 1369px content width */
.about__intro { display: grid; grid-template-columns: 694fr 611fr; gap: 64px; margin-top: 72px; align-items: start; }
.about__bio p { margin: 0 0 28px; font-size: 24px; font-weight: 500; line-height: 1.3; letter-spacing: -0.03em; }
.about__portrait { width: 100%; aspect-ratio: 0.8767; object-fit: cover; }

/* ---------- 404 ---------- */

.notfound { min-height: 50vh; display: grid; place-content: center; text-align: center; gap: 16px; }

/* ---------- responsive ---------- */

@media (max-width: 900px) {
  :root { --pad: 20px; --gap: 16px; }
  .grid--even, .grid--feature, .grid--thirds, .gallery__pair { grid-template-columns: 1fr; }
  .strip, .about__intro { grid-template-columns: 1fr; gap: 32px; }
  .statement { font-size: 40px; margin-top: 72px; }
  .project__title, .section-title { font-size: 56px; }
  .project__overview { font-size: 26px; }
  .hero__tagline { font-size: 18px; margin-left: 0; }
  .meta { flex-wrap: wrap; gap: 24px; }
  .meta > div { flex: 1 1 45%; }
  .exp__row { grid-template-columns: 1fr; gap: 4px; }
  .post-link { grid-template-columns: 1fr; gap: 6px; }
  .article__title { font-size: 44px; max-width: none; }
  .prose { font-size: 18px; margin-top: 40px; }
  .prose h2 { font-size: 26px; }
  .prose blockquote { font-size: 20px; }
  .footer__top { flex-direction: column; }
  .filters { font-size: 18px; gap: 16px; }
  .card__title { font-size: 20px; }
  .grid { margin-top: 56px; }
}

@media (prefers-reduced-motion: no-preference) {
  a, button { transition: opacity 0.2s ease; }
}

/* ---------- entrance animations ----------

   The original runs Framer "appear" effects on page load: elements slide up
   and fade in. No scroll triggers, and no hover animations anywhere.

   Its springs are stiffness 400 / mass 1 / damping 60-80, which is a damping
   ratio of 1.5-2.0 — overdamped, so no bounce, just a fast approach and a
   long settle. cubic-bezier(0.16, 1, 0.3, 1) tracks that closely: measured
   against the original, both cover ~92% of the distance in the first 250ms.

   Per-page mapping from the original:
     home    hero wordmark  y=1500 delay 0      | tagline y=100 delay 0.75s
     work    heading        y=120  delay 0.7s   | content y=120 delay 0.9s
     about   heading        y=120  delay 0.7s   | content y=120 delay 0.9s
     project (no visible entrance animation)

   --rise-y is capped at 120px for the hero rather than the original's 1500px:
   translating a full-width heading 1500px down extends the document and flashes
   a scrollbar on load. Set --rise-y: 1500px on .hero .display to restore it. */

.rise {
  --rise-y: 120px;
  --rise-delay: 0s;
  animation: rise 0.75s cubic-bezier(0.16, 1, 0.3, 1) var(--rise-delay) backwards;
}
.rise--tagline { --rise-y: 100px; --rise-delay: 0.75s; }
.rise--heading { --rise-delay: 0.7s; }
.rise--content { --rise-delay: 0.9s; }

@keyframes rise {
  from { opacity: 0; transform: translateY(var(--rise-y)); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Motion off: content is simply present, no delay before it appears. */
@media (prefers-reduced-motion: reduce) {
  .rise { animation: none; }
}

/* ---------- custom cursor ----------

   A dot that follows the pointer and swells into a "see more" disc over the
   home page's project cards. Only for real pointers: no mouse means no
   cursor, and it stays off when motion is reduced. The native cursor is
   hidden only while ours is actually on screen, so nothing is ever left
   without a pointer.

   Position comes from --x/--y so movement is a transform, never layout. */

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--fg);
  color: var(--bg);
  opacity: 0;
  pointer-events: none;

  /* difference blend: result = |backdrop - source|. The near-white disc
     inverts whatever it passes over — light grey on the dark ground, dark
     over a bright photo — so it always separates from its backdrop. The
     near-black label inverts to roughly the backdrop colour, which reads as
     the text being knocked out of the disc. */
  mix-blend-mode: difference;
  transform: translate3d(var(--x, -100px), var(--y, -100px), 0) translate(-50%, -50%);
  transition:
    width 0.3s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.3s cubic-bezier(0.16, 1, 0.3, 1),
    opacity 0.2s ease;
}
.cursor--on   { opacity: 1; }
.cursor--more { width: 132px; height: 132px; }

.cursor__label {
  font-size: 20px;
  font-weight: 500;
  letter-spacing: -0.04em;
  opacity: 0;
  transition: opacity 0.2s ease;
  white-space: nowrap;
}
.cursor--more .cursor__label { opacity: 1; transition-delay: 0.06s; }

body.cursor-hidden { cursor: none; }
body.cursor-hidden a,
body.cursor-hidden button { cursor: none; }

/* ---------- scroll-linked brightening ----------

   The original mutes two blocks and brightens them to full as they travel up
   the viewport. Measured on the live site:
     home  .statement   split into 31 word elements, each 0.4 -> 1
     about bio paragraphs, muted as whole blocks,         0.6 -> 1
   Colour is constant throughout; only opacity changes.

   Built on scroll-driven animations, so no JavaScript. Giving each word its
   own view() timeline makes the block fill progressively as lines rise. */

@property --i { syntax: "<number>"; initial-value: 0; inherits: false; }

.fill .fill-word { --fill-from: 0.4; }
.fill-block      { --fill-from: 0.6; }

@supports (animation-timeline: view()) {
  .fill .fill-word,
  .fill-block {
    opacity: var(--fill-from);
    animation: fill linear both;
    animation-timeline: view();
    /* Whole-block default. No word-level stagger here — the original mutes
       the bio as whole paragraphs. Each paragraph carries its own view()
       timeline, so the three still light in sequence as each comes up. */
    animation-range:
      cover var(--fill-block-start)
      cover calc(var(--fill-block-start) + var(--fill-block-window));
  }

  /* Stagger each word by its index so the block fills word by word.
     --fill-window: how fast one word brightens. Smaller = harder snap.
     --fill-step:   scroll distance between consecutive words. Bigger = the
                    sweep is strung out over more scrolling.
     The two work against each other: a small window with a large step gives
     a crisp edge travelling slowly through the text, rather than the whole
     block fading up together. */
  .fill .fill-word {
    animation-range:
      cover calc(var(--fill-start) + var(--i) * var(--fill-step))
      cover calc(var(--fill-start) + var(--fill-window) + var(--i) * var(--fill-step));
  }
}

@keyframes fill { to { opacity: 1; } }

/* Without scroll-timeline support, and whenever motion is reduced, the text
   simply sits at full opacity — never stranded in its muted state. */
@media (prefers-reduced-motion: reduce) {
  .fill .fill-word,
  .fill-block { opacity: 1; animation: none; }
}
