/* ---------------------------------------------------------------------------
   Noah Anleitner — design system

   One handwritten family throughout, on a ground that carries depth rather
   than a flat fill. Everything below is driven by the tokens in this first
   block: both themes are the same design with different values, so nothing
   downstream ever asks which theme is active.
   --------------------------------------------------------------------------- */

:root {
  /* Ground and surfaces. The ground is never a flat colour on screen — the
     glows in .aura sit on top of it — so it stays deliberately plain here. */
  --ground: #f7f6fb;
  --ground-deep: #eceaf5;
  --surface: rgba(20, 18, 28, 0.04);
  --surface-strong: rgba(20, 18, 28, 0.07);
  --edge: rgba(20, 18, 28, 0.12);

  --ink: #14121c;
  --ink-soft: #4b4661;
  --ink-faint: #6f6987;

  /* The brand pair. -ink variants are the same hues pulled dark enough to
     read as text on a light ground, where the plain cyan cannot. */
  --violet: #8c0be8;
  --cyan: #00c6ff;
  --violet-ink: #7209c0;
  --cyan-ink: #0479a3;

  --glow-violet: rgba(140, 11, 232, 0.18);
  --glow-cyan: rgba(0, 198, 255, 0.16);

  --shadow-low: 0 2px 10px rgba(20, 18, 28, 0.08);
  --shadow-high: 0 24px 60px rgba(20, 18, 28, 0.16);

  /* Fluid type. Each step interpolates between a phone and a wide desktop, so
     there are no breakpoint jumps in the headings. */
  --step-display: clamp(3rem, 1.2rem + 7.5vw, 8rem);
  --step-title: clamp(2.25rem, 1.4rem + 3.6vw, 4.5rem);
  --step-heading: clamp(1.5rem, 1.2rem + 1.4vw, 2.25rem);
  --step-lead: clamp(1.125rem, 1rem + 0.6vw, 1.5rem);
  --step-body: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
  --step-small: 0.875rem;

  --measure: 34rem;
  --shell: 78rem;
  --gutter: clamp(1.25rem, 0.6rem + 3vw, 4rem);
  --rhythm: clamp(4rem, 2rem + 8vw, 9rem);

  --radius: 22px;
  --radius-lg: 32px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --ground: #0b0a12;
    --ground-deep: #07060d;
    --surface: rgba(255, 255, 255, 0.045);
    --surface-strong: rgba(255, 255, 255, 0.08);
    --edge: rgba(255, 255, 255, 0.12);

    --ink: #f4f2fb;
    --ink-soft: #b6b1c9;
    --ink-faint: #8b85a3;

    /* On a dark ground the pure hues are the readable ones. */
    --violet-ink: #b06bff;
    --cyan-ink: #4bd8ff;

    --glow-violet: rgba(140, 11, 232, 0.3);
    --glow-cyan: rgba(0, 198, 255, 0.22);

    --shadow-low: 0 2px 12px rgba(0, 0, 0, 0.5);
    --shadow-high: 0 24px 70px rgba(0, 0, 0, 0.7);
  }
}

/* ---------------------------------------------------------------------------
   The face. One variable file carries 300–800, so weight is a free axis and
   headings need no second family.
   --------------------------------------------------------------------------- */

@font-face {
  font-family: 'Shantell Sans';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url(fonts/shantell-sans-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Shantell Sans';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url(fonts/shantell-sans-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------------------------------------------------------------------------
   Base
   --------------------------------------------------------------------------- */

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

html {
  scroll-behavior: smooth;
  /* Anchored sections stop below the bar rather than under it. */
  scroll-padding-top: 5.5rem;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--ground);
  color: var(--ink);
  font-family: 'Shantell Sans', 'Segoe Print', 'Comic Sans MS', cursive;
  font-size: var(--step-body);
  font-weight: 400;
  /* Handwriting needs more air between lines than a neutral sans to stay
     readable at paragraph length. */
  line-height: 1.75;
  /* clip rather than hidden: hidden leaves the page scrollable sideways by
     however far the rings reach past the edge, and a page that can be dragged
     27px to the right is how a fixed bar ends up looking cut off. clip crops
     them without making anything scrollable. */
  overflow-x: clip;
}

h1, h2, h3, p, ul, figure {
  margin: 0;
}

ul {
  padding: 0;
  list-style: none;
}

img, svg {
  display: block;
  max-width: 100%;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

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

/* ---------------------------------------------------------------------------
   Layout
   --------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  position: relative;
  padding-block: var(--rhythm);
}

/* Every section of the page proper stands as its own panel: at least a screen
   tall with its content centred. Sections carrying more than a screen — the
   five project tiles, the four posts — simply grow past it. svh rather than vh
   so a phone's collapsing address bar does not change the height mid-scroll. */
main > .section {
  min-height: 100svh;
  display: flex;
  align-items: center;
  /* Cropped here rather than left to the page: the rings reach past the side
     on a narrow screen, which is the look, but anything hanging off the body
     leaves the page draggable sideways by that much. clip, not hidden, so it
     crops without becoming a scroll container — sticky and the aura behind it
     carry on as before. */
  overflow-x: clip;
}

.lede {
  max-width: var(--measure);
  color: var(--ink-soft);
  font-size: var(--step-lead);
}

.prose {
  max-width: var(--measure);
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------------------
   Type
   --------------------------------------------------------------------------- */

.display {
  font-size: var(--step-display);
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: -0.02em;
}

.title {
  font-size: var(--step-title);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.01em;
}

.heading {
  font-size: var(--step-heading);
  font-weight: 600;
  line-height: 1.2;
}

/* Small caps-ish label. Handwriting has no small-caps, so the effect comes
   from size, weight and tracking instead. */
.label {
  font-size: var(--step-small);
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.accent {
  background-image: linear-gradient(100deg, var(--violet-ink), var(--cyan-ink));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ---------------------------------------------------------------------------
   Aura — the ground's depth. Two soft radial fields, deliberately larger than
   the viewport so their edges never show, sitting behind everything.
   --------------------------------------------------------------------------- */

.aura {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background: radial-gradient(120% 80% at 50% 0%, var(--ground-deep), var(--ground) 60%);
}

.aura::before,
.aura::after {
  content: '';
  position: absolute;
  width: 70vmax;
  height: 70vmax;
  border-radius: 50%;
  filter: blur(60px);
}

.aura::before {
  top: -30vmax;
  left: -20vmax;
  background: radial-gradient(circle, var(--glow-violet), transparent 65%);
}

.aura::after {
  top: 30vmax;
  right: -30vmax;
  background: radial-gradient(circle, var(--glow-cyan), transparent 65%);
}

/* ---------------------------------------------------------------------------
   Bar
   --------------------------------------------------------------------------- */

.bar {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 40;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  background-color: color-mix(in srgb, var(--ground) 72%, transparent);
  border-bottom: 1px solid transparent;
  transition: border-color 0.4s var(--ease), background-color 0.4s var(--ease);
}

.bar[data-scrolled='true'] {
  border-bottom-color: var(--edge);
}

.bar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(0.75rem, 0.4rem + 1.2vw, 1.5rem);
  min-height: clamp(3.75rem, 3rem + 2vw, 4.5rem);
}

/* Whatever else gives, the way to reach you does not. */
.bar .button {
  flex: none;
}

.bar__mark {
  font-weight: 700;
  /* Fluid rather than stepped at a breakpoint, so there is no width at which
     it is still large while the room has already gone. */
  font-size: clamp(1rem, 0.85rem + 0.6vw, 1.25rem);
  letter-spacing: -0.01em;
  /* A flex item will not shrink below its own content unless told it may. The
     name gives way before the button does, and truncates rather than pushing
     it off the edge — which is what happens while the handwriting is still
     loading and the fallback face runs wider than it will. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bar__nav {
  display: none;
  gap: 1.75rem;
}

@media (min-width: 56rem) {
  .bar__nav {
    display: flex;
  }
}

.bar__link {
  position: relative;
  font-size: var(--step-small);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-block: 0.35rem;
  transition: color 0.3s var(--ease);
}

.bar__link::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: 2px;
  border-radius: 2px;
  background-image: linear-gradient(90deg, var(--violet-ink), var(--cyan-ink));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease);
}

.bar__link:hover,
.bar__link[aria-current='true'] {
  color: var(--ink);
}

.bar__link:hover::after,
.bar__link[aria-current='true']::after {
  transform: scaleX(1);
}

/* ---------------------------------------------------------------------------
   Buttons
   --------------------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  /* Two words on two lines make a bar twice as tall on a phone. */
  white-space: nowrap;
  padding: clamp(0.5rem, 0.4rem + 0.4vw, 0.7rem) clamp(1rem, 0.6rem + 1.4vw, 1.5rem);
  font-size: clamp(0.9375rem, 0.9rem + 0.2vw, 1rem);
  border: 0;
  border-radius: 999px;
  font-weight: 600;
  /* A real <button> carries the browser's own opaque buttonface underneath.
     The gradient hides it, but anything reading the background colour still
     finds it — the cursor's luminance probe read this button as a light
     surface and drew itself dark on a dark page. Stating the colour the
     gradient starts from replaces it with the truth. */
  background-color: var(--violet);
  background-image: linear-gradient(100deg, var(--violet), var(--cyan));
  color: #fff;
  box-shadow: var(--shadow-low);
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-high);
}

.button--quiet {
  background-image: none;
  /* Frosted rather than merely tinted: the ring behind the hero read straight
     through a surface this faint, so what passes behind is diffused instead. */
  background-color: color-mix(in srgb, var(--ground) 60%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--ink);
  border: 1px solid var(--edge);
  box-shadow: none;
}

.button--quiet:hover {
  background-color: var(--surface-strong);
}

/* ---------------------------------------------------------------------------
   Hero
   --------------------------------------------------------------------------- */

.hero {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  padding-block-start: 6rem;
}

@media (min-width: 62rem) {
  .hero {
    grid-template-columns: 1.15fr 0.85fr;
  }
}

.hero__title {
  margin-block: 0.5rem 1.5rem;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin-block-start: 2.25rem;
}

.hero__art {
  position: relative;
  justify-self: center;
  /* The rings sit 18% outside this box and are meant to run past the edge of a
     narrow screen; the page's overflow rule crops them, which is the look. */
  width: min(100%, 26rem);
}

.hero__art img {
  width: 100%;
}

/* ---------------------------------------------------------------------------
   Section headings, with a drawn rule beneath. The stroke is an SVG path so it
   can be animated later; here it simply sits under the words.
   --------------------------------------------------------------------------- */

.section__head {
  margin-block-end: clamp(2.5rem, 5vw, 4rem);
}

.section__rule {
  width: clamp(8rem, 18vw, 14rem);
  height: 14px;
  margin-block-start: 0.75rem;
  overflow: visible;
}

.section__rule path {
  fill: none;
  stroke: url(#rule-gradient);
  stroke-width: 4;
  stroke-linecap: round;
}

/* ---------------------------------------------------------------------------
   Motion is opt-out at the root: anything that moves is written so that this
   single block can switch it off.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------------------------------------------------------------------
   Cards — one per project, running down the page. The mark sits in its own
   column beside the words on a wide screen and above them on a narrow one.
   --------------------------------------------------------------------------- */

/* The drawn outline is the tile. It used to sit inside a translucent panel
   with a radius of its own, and the two edges read as a double border — the
   panel is gone, so there is one line around a tile and it is a drawn one.
   The border below is only what shows before the outline is injected. */
.tile {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  padding: clamp(1.5rem, 3vw, 2.75rem);
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  transition: transform 0.4s var(--ease), border-color 0.4s var(--ease);
}

.tile:hover {
  transform: translateY(-4px);
}

@media (min-width: 48rem) {
  .tile {
    grid-template-columns: 10rem 1fr;
    align-items: center;
  }
}

.tile + .tile {
  margin-block-start: clamp(1rem, 2.5vw, 2rem);
}

.tile__media {
  display: flex;
  align-items: center;
  justify-content: center;
}

.tile__media img {
  width: min(100%, 8.5rem);
}

/* A tile for marks that carry their own opaque background and would otherwise
   sit as a hard rectangle on the surface. */
.tile__media--plate img {
  padding: 0.75rem;
  border-radius: 18px;
  background-color: #fff;
}

.tile__media--rounded img {
  border-radius: 22%;
}

/* Retired work reads as retired: colour drains out and returns on hover. */
.tile__media--past img {
  filter: grayscale(1);
  opacity: 0.55;
  transition: filter 0.4s var(--ease), opacity 0.4s var(--ease);
}

.tile:hover .tile__media--past img {
  filter: grayscale(0);
  opacity: 1;
}

.tile__title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.tile__body {
  /* A tile is wider than a comfortable line, so the words stop before it does. */
  max-width: 62ch;
  margin-block: 0.75rem 0;
  color: var(--ink-soft);
}

.tile__note {
  max-width: 62ch;
  margin-block-start: 0.5rem;
  color: var(--ink-faint);
  font-size: var(--step-small);
}

/* ---------------------------------------------------------------------------
   Badges, links and tags
   --------------------------------------------------------------------------- */

.badge {
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background-color: var(--surface-strong);
  color: var(--ink-faint);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
}

.meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin-block-start: 1.25rem;
}

.meta a {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 600;
  color: var(--ink);
  border-bottom: 2px solid transparent;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}

.meta a:hover {
  color: var(--violet-ink);
  border-bottom-color: var(--violet-ink);
}

.icon {
  width: 1.15em;
  height: 1.15em;
  fill: currentColor;
  flex: none;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-block-start: 1.25rem;
}

.tags li {
  padding: 0.3rem 0.85rem;
  border: 1px solid var(--edge);
  border-radius: 999px;
  font-size: var(--step-small);
  color: var(--ink-soft);
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}

.tags li:hover {
  border-color: var(--cyan-ink);
  color: var(--ink);
}

/* ---------------------------------------------------------------------------
   About — short blocks beside the tag list
   --------------------------------------------------------------------------- */

.about {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
}

@media (min-width: 52rem) {
  .about {
    grid-template-columns: 1fr 1fr;
    align-items: start;
  }
}

.about__block + .about__block {
  margin-block-start: 1.75rem;
}

.about__block h3 {
  font-size: 1.125rem;
  font-weight: 700;
}

.about__block p {
  margin-block-start: 0.35rem;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------------------
   Garage — stand-ins until a visitor asks for the posts, then the frames the
   embeds are set into. Instagram's document is always white and beyond reach
   from here, so it is framed rather than fought.
   --------------------------------------------------------------------------- */

.posts {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.75rem);
  /* The consent note sits directly above this once the button has gone. */
  margin-block-start: clamp(2rem, 4vw, 3.5rem);
  /* Frames are as tall as their post, not as tall as the tallest in the row. */
  align-items: start;
}

@media (min-width: 48rem) {
  .posts {
    grid-template-columns: 1fr 1fr;
  }
}

.post-stub {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  min-height: 22rem;
  padding: 2rem;
  border: 1px dashed var(--edge);
  border-radius: var(--radius-lg);
  text-align: center;
  color: var(--ink-faint);
}

.post-stub svg:not(.drawn) {
  width: 2.5rem;
  height: 2.5rem;
  fill: var(--violet-ink);
  /* The mark drifts while it waits. It is the mark rather than the stub that
     moves, so this cannot fight the reveal transform on the stub itself. */
  animation: bob 4.5s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * -0.9s);
}

@keyframes bob {
  0%, 100% { transform: translateY(-5px); }
  50% { transform: translateY(5px); }
}

.post-stub {
  transition: border-color 0.4s var(--ease), background-color 0.4s var(--ease),
              transform 0.4s var(--ease);
}

.post-stub:hover {
  border-color: var(--violet-ink);
  background-color: var(--surface);
  transform: translateY(-4px);
}

.post-stub strong {
  color: var(--ink);
  font-weight: 700;
}

/* No surface of its own: a panel behind the post only showed up as a slab
   wherever the embed was shorter than its neighbour. The corners are rounded
   on the iframe itself, which belongs to this page even though its contents
   do not. */
.post-frame {
  max-width: 564px;
  margin-inline: auto;
  opacity: 0;
  transform: translateY(22px) scale(0.98);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
  transition-delay: calc(var(--i, 0) * 130ms);
}

.post-frame.is-in {
  opacity: 1;
  transform: none;
}

.post-frame.is-in:hover {
  transform: translateY(-6px);
}

.instagram-media {
  border: 0 !important;
  border-radius: 20px !important;
  box-shadow: none !important;
}

/* ---------------------------------------------------------------------------
   Footer
   --------------------------------------------------------------------------- */

.footer {
  margin-block-start: var(--rhythm);
}

.footer__grid {
  display: grid;
  gap: 2rem;
}

@media (min-width: 40rem) {
  .footer__grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.footer__grid ul {
  display: grid;
  gap: 0.6rem;
  margin-block-start: 0.9rem;
}

.footer__grid a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--ink-soft);
  transition: color 0.3s var(--ease);
}

.footer__grid a:hover {
  color: var(--ink);
}

/* A tile with no mark of its own drops the media column entirely. */
@media (min-width: 48rem) {
  .tile--plain {
    grid-template-columns: 1fr;
  }
}

/* ---------------------------------------------------------------------------
   Motion

   Everything that moves is declared here and switched on by a class the
   observer adds, so the reduced-motion block at the end of this file — and the
   matching guard in the script — can silence the lot without hunting.
   --------------------------------------------------------------------------- */

[data-reveal] {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
  /* Children of one group step in one after another rather than together. */
  transition-delay: calc(var(--i, 0) * 90ms);
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* The rule under a heading draws itself. Its dash length is measured from the
   path in the script, since a guessed one either clips or leaves a gap. */
.section__rule path {
  transition: stroke-dashoffset 1.2s var(--ease) 0.2s;
}

[data-reveal].is-in .section__rule path {
  stroke-dashoffset: 0 !important;
}

/* The thread down the side, drawn in proportion to how far the page has been
   read. It is decoration, so it goes when the screen is too narrow to spare
   the room and when motion is unwelcome. */
.thread {
  position: fixed;
  inset-block: 0;
  inset-inline-start: clamp(0.75rem, 2.2vw, 2.5rem);
  width: 42px;
  height: 100%;
  z-index: 5;
  pointer-events: none;
  opacity: 0.55;
}

.thread path {
  fill: none;
  stroke: url(#rule-gradient);
  stroke-width: 3;
  stroke-linecap: round;
}

@media (max-width: 64rem) {
  .thread {
    display: none;
  }
}

/* The glows drift against the scroll. Pseudo-elements cannot be reached from
   the script, so it writes these two properties on the parent instead. */
.aura::before {
  transform: translate3d(0, var(--drift-a, 0), 0);
}

.aura::after {
  transform: translate3d(0, var(--drift-b, 0), 0);
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    opacity: 1;
    transform: none;
  }

  .thread {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   Boil

   Hand-drawn linework in animation is never one drawing: it is two, swapped a
   few times a second, and the wobble between them is what stops a stroke
   looking like it came out of a machine. Every sketched outline here is a pair
   of paths taking turns.

   preserveAspectRatio is off so an outline stretches to whatever it wraps, and
   non-scaling-stroke keeps the line an even weight while it does.
   --------------------------------------------------------------------------- */

.drawn {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}

/* The fill rides on the drawn path rather than on the element, so the surface
   and the line around it are one shape and cannot disagree — the same wobble
   bounds both, and there is no CSS radius for the stroke to miss. */
.drawn path {
  fill: var(--surface);
  stroke: color-mix(in srgb, var(--ink) 22%, transparent);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  transition: stroke 0.4s var(--ease), fill 0.4s var(--ease);
}

/* Behind the words, but only within its own tile: the stacking context is what
   keeps a negative index from falling through to the section. */
.drawn {
  z-index: -1;
}

.tile,
.post-stub {
  isolation: isolate;
}

.tile--sketched:hover .drawn path,
.post-stub--sketched:hover .drawn path {
  fill: var(--surface-strong);
}

/* Each outline is drawn at its own pace and out of step with its neighbours;
   the values come from the script, which knows each tile's position. */
.drawn path:first-child {
  animation: boil-a var(--boil, 0.3s) steps(1, end) infinite;
  animation-delay: var(--boil-shift, 0s);
}

.drawn path:last-child {
  animation: boil-b var(--boil, 0.3s) steps(1, end) infinite;
  animation-delay: var(--boil-shift, 0s);
}


@keyframes boil-a {
  0%, 49.9% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

@keyframes boil-b {
  0%, 49.9% { opacity: 0; }
  50%, 100% { opacity: 1; }
}

/* Cards and stubs wear the outline instead of a border once the script has
   drawn one; the border stays as the fallback until then. */
.tile,
.post-stub {
  position: relative;
}

.tile--sketched,
.tile--sketched:hover,
.post-stub--sketched,
.post-stub--sketched:hover {
  border-color: transparent;
}

.tile--sketched:hover .drawn path,
.post-stub--sketched:hover .drawn path {
  stroke: var(--violet-ink);
}

/* ---------------------------------------------------------------------------
   Behind the mark — sketched rings, turning at their own speeds and against
   each other, with the same two-state wobble.
   --------------------------------------------------------------------------- */

.hero__art {
  position: relative;
}

.rings {
  position: absolute;
  inset: -18%;
  z-index: -1;
  pointer-events: none;
}

.rings svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.rings path {
  fill: none;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.rings__outer {
  animation: turn 46s linear infinite;
  transform-origin: 50% 50%;
}

.rings__middle {
  animation: turn 32s linear infinite reverse;
  transform-origin: 50% 50%;
}

.rings__inner {
  animation: turn 22s linear infinite;
  transform-origin: 50% 50%;
}

@keyframes turn {
  to { transform: rotate(360deg); }
}

/* ---------------------------------------------------------------------------
   Behind the projects — a drawn field that drifts, far enough back to read as
   paper texture rather than as content.
   --------------------------------------------------------------------------- */

.field {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  opacity: 0.85;
  mask-image: radial-gradient(75% 60% at 50% 50%, #000, transparent 75%);
  -webkit-mask-image: radial-gradient(75% 60% at 50% 50%, #000, transparent 75%);
}

.field svg {
  width: 160%;
  height: 160%;
  margin-left: -30%;
  margin-top: -30%;
  animation: drift 34s ease-in-out infinite alternate;
}

.field path {
  fill: none;
  /* Stronger than a plain edge: masked and behind content, a hairline simply
     disappears. */
  stroke: color-mix(in srgb, var(--violet-ink) 38%, transparent);
  stroke-width: 1.4;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

@keyframes drift {
  from { transform: translate3d(-1.5%, -1%, 0) rotate(-0.6deg); }
  to { transform: translate3d(1.5%, 1%, 0) rotate(0.6deg); }
}

/* The name's gradient never quite settles. */
.accent {
  background-size: 220% 100%;
  animation: wander 14s ease-in-out infinite alternate;
}

@keyframes wander {
  from { background-position: 0% 50%; }
  to { background-position: 100% 50%; }
}

@media (prefers-reduced-motion: reduce) {
  /* One drawing rather than two, and nothing turning or drifting. */
  .drawn path:last-child {
    opacity: 0;
  }

  .rings,
  .field {
    display: none;
  }
}

/* Ring strokes. Each ring is itself a pair of drawings, so the wobble runs
   inside a group that is separately being turned. */
.rings .b {
  opacity: 0;
}

.rings .a {
  animation: boil-a 0.52s steps(1, end) infinite;
}

.rings .b {
  animation: boil-b 0.52s steps(1, end) infinite;
}

.rings__outer path {
  stroke: color-mix(in srgb, var(--violet-ink) 55%, transparent);
  stroke-width: 2;
}

.rings__middle path {
  stroke: color-mix(in srgb, var(--cyan-ink) 45%, transparent);
  stroke-width: 1.6;
  stroke-dasharray: 5 13;
}

.rings__inner path {
  stroke: color-mix(in srgb, var(--violet-ink) 32%, transparent);
  stroke-width: 1.4;
  stroke-dasharray: 2 10;
}

/* ---------------------------------------------------------------------------
   Phones. The bar is the tightest row on the page: a name and a button, with
   nothing to give.
   --------------------------------------------------------------------------- */

@media (max-width: 30rem) {
  .hero {
    padding-block-start: 4.5rem;
  }
}
