/* ==========================================================================
   aaranbeattie.com — css
   --------------------------------------------------------------------------
   Sections
     1. Tokens
     2. Base
     3. Links
     4. Page
     5. Field
     6. Motion
   ========================================================================== */

/* ==========================================================================
   1. Tokens
   ========================================================================== */

:root {
  color-scheme: light;

  /* --- Type scale --------------------------------------------------------- */
  --font-fallback: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Arial, sans-serif;
  --font-display: "neue-haas-grotesk-display", var(--font-fallback);
  --font-text: "neue-haas-grotesk-text", var(--font-fallback);
  --weight-regular: 400;
  --weight-medium: 500;

  --size-floor: 0.875rem;
  --size-small: var(--size-floor);
  --size-footer: 0.875rem;
  --size-ui: 1rem;
  --size-text: 1.1875rem;
  --size-page-title: var(--size-ui);
  --size-lead: clamp(1.5rem, 1.35rem + 0.6vw, 1.75rem);
  --size-h2: 1.5rem;
  --size-h3: 1.25rem;
  --size-h4: var(--size-text);
  --size-meta: var(--size-ui);

  --tracking-h2: 0;
  --tracking-h3: 0;
  --tracking-h4: 0;
  --tracking-text: 0;
  --tracking-ui: 0.008em;
  --tracking-utility: 0.05em;
  --leading-flat: 1;
  --leading-h2: 1.2;
  --leading-h3: 1.3;
  --leading-h4: 1.35;
  --leading-lead: 1.4;
  --leading-text: 1.6;
  --leading-ui: 1.4;

  /* --- Palette ------------------------------------------------------------- */
  --white: #ffffff;
  --black: #000000;
  --accent: #4643B8;
  --accent-dark: #7E87F5;
  --graphite: #6f6f6f;
  --hairline: #dcdcdc;
  --paper-dark: #101010;
  --ink-dark: #EAE8E5;
  --graphite-dark: #8c8c8c;
  --hairline-dark: #3a3a3a;

  --color-bg: var(--white);
  --color-fg: var(--black);
  --color-mark: var(--black);
  --color-muted: var(--graphite);
  --color-rule: var(--hairline);
  --color-accent: var(--accent);

  /* The field is a wash, not a mark, so it has its own accent. On paper the
         two are the same value. On a dark page they part: a mark has to lift off
         #101010 to be seen, which sets a floor on its lightness, while a wash
         becomes the ground the text sits on, which sets a ceiling. The windows
         do not overlap at any hue, so one value cannot do both. */
  --field-accent: var(--accent);

  --color-selection: color-mix(in srgb, var(--color-accent) 20%, transparent);
  --color-rule-hover: color-mix(in srgb, var(--color-fg) 45%, var(--color-bg));

  /* --- Spacing ------------------------------------------------------------ */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2rem;
  --space-xl: 3rem;
  --space-2xl: 4rem;
  --space-3xl: 6rem;
  --space-4xl: 9rem;

  --flow-space: var(--space-md);

  /* --- Layout -------------------------------------------------------------- */
  --space-edge: clamp(1.25rem, 3.6vw, 2.5rem);
  --page-max: 76rem;
  --content-width: 64rem;
  --gutter: clamp(1.5rem, 2.6vw, 2rem);

  --width-xs: 20rem;
  --width-sm: 28rem;
  --width-md: 36rem;
  --width-lg: 44rem;

  /* --- Marks --------------------------------------------------------------- */
  --rule-weight: 1px;
  --focus-offset: 0.4em;
  --link-rule-weight: 1px;
  --link-rule-offset: 0.18em;
  --link-rule-offset-ui: 0.35em;

  /* --- Motion -------------------------------------------------------------- */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 160ms;
  --dur-mid: 420ms;

  /* --- Page ---------------------------------------------------------------- */
  --holding-measure: 32rem;

  --gap-name: var(--space-2xs);
  --gap-role: var(--space-md);
  --gap-links: var(--space-xl);
  --gap-link-item: var(--space-xs);

  --holding-arrow-size: 1em;
  --holding-arrow-stroke: 1.75;
  --holding-arrow-shift: var(--space-3xs);

  --holding-link-offset: var(--link-rule-offset-ui);

  /* --- Field ---------------------------------------------------------------
         The blur is split between the canvas buffer and this filter: the buffer is
         drawn at a fraction of display size, so the upscale supplies most of the
         softness and the filter only finishes it. */
  --field-blur: 1.25rem;

  /* Without a mask the field has to be quiet everywhere, since the words sit
         on it. The @supports block below raises this wherever masks are
         supported. */
  --field-opacity: 0.5;

  /* What is left of the field where the words are. The accent runs at full
         weight across the open page and is held back only here. */
  --field-quiet: 0.13;
  --field-quiet-size: 42rem 30rem;

  /* --- Field, without script ------------------------------------------------
         One bloom held at the middle of its life. It starts at nothing and is
         faded in on a delay, so a page whose script is on its way never shows
         it: field.js announces itself long before the delay is out. A script
         that is blocked, missing or broken never announces anything, and the
         bloom arrives instead. Placed away from the words. */
  --field-still-x: 66%;
  --field-still-y: 58%;
  /* The radius, not the width. */
  --field-still-size: 24vmax;

  /* The weight a bloom carries halfway through its life, which is the moment
         this one is held at. */
  --field-still-core: color-mix(in srgb, var(--field-accent) 30%, transparent);
  --field-still-mid: color-mix(in srgb, var(--field-accent) 28%, transparent);
  --field-still-edge: color-mix(in srgb, var(--field-accent) 18%, transparent);

  /* In together, these land the bloom at about the moment the script would
         have started the field, so the page has the same shape either way. */
  --field-still-delay: 700ms;
  --field-still-dur: 900ms;

  /* How far the canvas runs past the viewport on each side. Blur samples what
         lies beyond an element's edge, and beyond the edge there is nothing, so a
         canvas cut to the viewport would fade out around its own border. */
  --field-bleed: calc(var(--field-blur) * 3);
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --color-bg: var(--paper-dark);
    --color-fg: var(--ink-dark);
    --color-mark: var(--ink-dark);
    --color-muted: var(--graphite-dark);
    --color-rule: var(--hairline-dark);
    --color-accent: var(--accent-dark);

    /* Marks take the lifted accent; the field keeps the true one, which sits
           below the text rather than climbing toward it. */
    --field-accent: var(--accent);

    /* Carried further, since the darker wash has the headroom for it. */
    --field-still-core: color-mix(in srgb, var(--field-accent) 40%, transparent);
    --field-still-mid: color-mix(in srgb, var(--field-accent) 38%, transparent);
    --field-still-edge: color-mix(in srgb, var(--field-accent) 24%, transparent);

    --field-opacity: 0.62;
  }
}

/* With the quiet zone in place the field can run at full weight, since the
       only thing it had to stay clear of was the text. */
@supports (mask-image: radial-gradient(#000, #000)) or (-webkit-mask-image: radial-gradient(#000, #000)) {
  :root {
    --field-opacity: 0.72;
  }

  @media (prefers-color-scheme: dark) {
    :root {
      --field-opacity: 0.85;
    }
  }
}

/* ==========================================================================
       2. Base
       ========================================================================== */

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

/* The page colour sits here rather than on the body, so the field can be laid
       behind the content without a body fill covering it. */
html {
  scrollbar-gutter: stable;
  -webkit-text-size-adjust: 100%;
  background-color: var(--color-bg);
}

body {
  margin: 0 auto;
  max-width: var(--page-max);
  min-height: 100svh;
  padding: var(--space-edge);
  display: grid;
  grid-template-rows: 1fr;
  align-content: start;
  color: var(--color-fg);
  font-family: var(--font-text);
  font-size: var(--size-ui);
  font-weight: var(--weight-regular);
  line-height: var(--leading-ui);
  letter-spacing: var(--tracking-ui);
  -webkit-font-smoothing: antialiased;
}

h1,
p,
ul {
  margin: 0;
}

::selection {
  background-color: var(--color-selection);
  color: var(--color-fg);
}

:focus-visible {
  outline: var(--rule-weight) solid var(--color-accent);
  outline-offset: var(--focus-offset);
  border-radius: 1px;
}

/* ==========================================================================
       3. Links
       ========================================================================== */

.link {
  color: inherit;
  text-decoration: none;
  padding-block-end: var(--link-rule-offset);
  background-image:
    linear-gradient(var(--color-accent), var(--color-accent)),
    linear-gradient(var(--color-rule), var(--color-rule));
  background-size: 0 var(--link-rule-weight), 100% var(--link-rule-weight);
  background-position: 0 100%, 0 100%;
  background-repeat: no-repeat;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-size var(--dur-mid) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.link:hover,
.link:focus-visible {
  background-size: 100% var(--link-rule-weight), 100% var(--link-rule-weight);
}

.link--ui {
  padding-block-end: var(--link-rule-offset-ui);
}

/* ==========================================================================
       4. Page
       ========================================================================== */

.holding__content {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--holding-measure);
}

.holding__name {
  font-size: var(--size-text);
  font-weight: var(--weight-medium);
  line-height: var(--leading-text);
  letter-spacing: var(--tracking-text);
}

.holding__role {
  margin-block-start: var(--gap-name);
  font-size: var(--size-text);
  line-height: var(--leading-text);
}

.holding__note {
  margin-block-start: var(--gap-role);
  font-size: var(--size-text);
  line-height: var(--leading-text);
  color: var(--color-muted);
  text-wrap: pretty;
}

.holding__links {
  margin-block-start: var(--gap-links);
  padding: 0;
  list-style: none;
  font-size: var(--size-ui);
}

.holding__links>li+li {
  margin-block-start: var(--gap-link-item);
}

.holding__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: inherit;
  text-decoration: none;
}

/* The underline is driven from the anchor, so it wipes in when the pointer is
       over the arrow as well as over the word. */
.holding__link:hover .link,
.holding__link:focus-visible .link {
  background-size: 100% var(--link-rule-weight), 100% var(--link-rule-weight);
}

/* The label carries the underline offset as bottom padding, which drops the
       centre of the pair below the text. An equal margin under the arrow lifts it
       back onto the word. */
.holding__arrow {
  width: var(--holding-arrow-size);
  height: var(--holding-arrow-size);
  margin-block-end: var(--holding-link-offset);
  color: var(--color-muted);
  stroke-width: var(--holding-arrow-stroke);
  transition: transform var(--dur-mid) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* The arrow carries the movement and the accent; the label stays put. */
.holding__link:hover .holding__arrow,
.holding__link:focus-visible .holding__arrow {
  transform: translateX(var(--holding-arrow-shift));
  color: var(--color-accent);
}

/* ==========================================================================
       5. Field
       ========================================================================== */

.field {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;

  /* Anchored to the text, not the viewport: the body is centred once the
         window passes --page-max, and the words travel with it. */
  --field-anchor-x: max(var(--space-edge), (100vw - var(--page-max)) / 2 + var(--space-edge));
  --field-anchor-y: var(--space-edge);

  -webkit-mask-image: radial-gradient(var(--field-quiet-size) at var(--field-anchor-x) var(--field-anchor-y),
      rgba(0, 0, 0, var(--field-quiet)) 0%,
      rgba(0, 0, 0, var(--field-quiet)) 18%,
      #000 100%);
  mask-image: radial-gradient(var(--field-quiet-size) at var(--field-anchor-x) var(--field-anchor-y),
      rgba(0, 0, 0, var(--field-quiet)) 0%,
      rgba(0, 0, 0, var(--field-quiet)) 18%,
      #000 100%);
}

/* A canvas is a replaced element, so it takes its intrinsic 300 by 150 when
       sized by insets alone. The bleed is stated in width and height instead. */
.field__canvas,
.field::after {
  position: absolute;
  top: calc(var(--field-bleed) * -1);
  left: calc(var(--field-bleed) * -1);
  display: block;
  width: calc(100% + var(--field-bleed) * 2);
  height: calc(100% + var(--field-bleed) * 2);
  filter: blur(var(--field-blur));
}

.field__canvas {
  opacity: var(--field-opacity);
}

/* The no-script bloom, on its own layer so its fade is its own and does not
       touch the canvas. Its stops are the ones the script draws. */
.field::after {
  content: "";
  opacity: 0;
  background-image: radial-gradient(circle var(--field-still-size) at var(--field-still-x) var(--field-still-y),
      var(--field-still-core) 0%,
      var(--field-still-mid) 62%,
      var(--field-still-edge) 85%,
      transparent 100%);
  animation: field-still var(--field-still-dur) var(--ease-out) var(--field-still-delay) forwards;
}

@keyframes field-still {
  to {
    opacity: var(--field-opacity);
  }
}

/* field.js sets this once it has the field. The layer is taken away rather
       than faded, since at that point it has not been seen. */
.field[data-js]::after {
  content: none;
}

/* ==========================================================================
       6. Motion
       The field holds a single still composition under this preference; the rest
       is handled here.
       ========================================================================== */

@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    transition-duration: 1ms !important;
  }
}

/* Without script the still bloom above is what the page keeps. Nothing here
       depends on field.js having run. */