/* ---------- Tokens ---------- */
:root {
  /* The page is a sky blue board and the key is a near white slab lying on
     it. Unlike the grey board it replaced, this ground is saturated enough to
     be a colour rather than a surface, so it pushes the key much further
     forward and the type sitting directly on it has to darken to keep its
     contrast. That is what --muted and --faint are doing below. */
  --bg: #a8d4f0;
  --fg: #0d1223;
  --muted: #3f5266;
  --faint: #445769;
  --accent: #1b4dff;

  /* The key face and everything printed on it. Flat, with no gradient of its
     own: every highlight on the slab comes from the two glass layers above
     it, so the face has to stay a single colour underneath them. */
  --key: #eef2f8;
  --key-ink: #12151c;
  --key-sub: #5c6478;
  --key-faint: #8d95a8;
  --key-rule: rgba(15, 23, 42, 0.1);

  --radius: 26px;
  --col: 600px;
  /* The press and the return are deliberately different lengths. A finger
     drives the press, so it answers fast. A spring drives the return, so it
     floats back. Both run on the same curve. */
  --down: 150ms;
  --up: 340ms;
  --ease: cubic-bezier(0.2, 0.75, 0.3, 1);
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

body {
  background: var(--bg);
  color: var(--fg);
  font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

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

/* ---------- Sky ---------- */
/* Three layers of drifting cloud behind the page, generated by sky.js.
   The ground stays the flat --bg it always was: there is no gradient in
   the sky, which is what keeps the tagline at 5.11 and the footer note at
   4.74 without either colour having to move. Clouds are white, so they
   only ever raise the luminance under the type and can only improve it.

   Depth comes from three things at once, and dropping any one of them
   flattens it: the parallax speeds, the per layer blur, and the grey
   underside on every puff. */
.sky {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;

  /* The ground is painted here as well as on body. This element is a
     stacking context, so the grain overlay blends against what is inside
     it and nothing else. With the blue only on body it sat outside the
     group, and away from a cloud the grain blended against transparency,
     which does nothing at all. */
  background: var(--bg);
}

/* A layer is twice the viewport wide and holds the same clouds twice, so
   sliding it exactly half its width returns it to where it started and the
   loop has no seam. Right to left, so clouds enter on the right edge. */
.sky__layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 200vw;
  height: 100%;
  will-change: transform;
  animation: sky-drift linear infinite;
  animation-duration: var(--dur);
}

@keyframes sky-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-100vw, 0, 0); }
}

/* A cloud is a bag of soft ellipses blurred into one mass. The blur is on
   the cloud rather than on each puff, so the puffs merge instead of each
   going soft on its own.

   The mask is what stops the mass having a silhouette. Without it the
   outermost puffs still describe an outline no matter how soft each one
   is, because the cloud is a rectangle and they stop where it stops. */
.cloud {
  position: absolute;
  filter: blur(var(--blur));
  -webkit-mask-image: radial-gradient(ellipse var(--fade) calc(var(--fade) + 2%) at 50% 50%, #000 22%, rgba(0, 0, 0, 0) 100%);
  mask-image: radial-gradient(ellipse var(--fade) calc(var(--fade) + 2%) at 50% 50%, #000 22%, rgba(0, 0, 0, 0) 100%);
}

/* A puff has no border radius and no solid fill. Its shape is a radial
   gradient that reaches zero before it reaches the element edge, so there
   is no boundary anywhere on it, only density falling off.

   The second gradient is a cool grey low in the puff. It is the only cue
   saying which way the light comes from, and it is deliberately weak. */
.puff {
  position: absolute;
  background:
    radial-gradient(closest-side ellipse at 50% 44%,
      rgba(255, 255, 255, 0.94) 0%,
      rgba(255, 255, 255, 0.6) 38%,
      rgba(255, 255, 255, 0) 78%),
    radial-gradient(closest-side ellipse at 50% 86%,
      rgba(203, 223, 239, 0.45) 0%,
      rgba(203, 223, 239, 0) 68%);
}

/* Texture over the whole sky. This began as dither against the banding a
   white to transparent falloff shows on a saturated ground, and it sits
   far past what dither needs, so it is texture now and meant to be.

   Overlay rather than normal blending, because it has to push a pixel both
   up and down. Plain noise at low opacity only ever darkens, which dims
   the sky instead of texturing it. */
.sky__grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--grain);
  mix-blend-mode: overlay;
  background-size: 140px 140px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------- Layout ---------- */
.page {
  width: 100%;
  max-width: var(--col);
  margin: 0 auto;
  padding: 72px 20px 96px;

  /* The page has to climb above the sky. Neither carried a stacking order
     before the sky existed. */
  position: relative;
  z-index: 1;
}

/* ---------- Header ---------- */
.header {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 15px;
  margin-bottom: 52px;
}

.header__avatar {
  width: 76px;
  height: 76px;
  border-radius: 50%;
  object-fit: cover;
  background: #eef2f8;
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.07);
}

/* Stands in for a photo. Flat colour, no gradient, nothing to load. */
.header__avatar--solid {
  background: var(--accent);
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.12);
}

.header__name {
  margin: 0;
  font-size: 27px;
  font-weight: 600;
  letter-spacing: -0.03em;
}

.header__tagline {
  margin: 0;
  max-width: 40ch;
  color: var(--muted);
  font-size: 14.5px;
}

.header__socials {
  display: flex;
  gap: 9px;
  margin-top: 5px;
}

/* A social link is the same glass key as a project, at small size.
   The face light and the wall bevels are folded into one box-shadow
   because the element carries no image of its own. */
.social {
  position: relative;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 13px;
  color: #8a92a6;
  background: linear-gradient(168deg, rgba(255, 255, 255, 0.95) 0%, rgba(238, 242, 250, 0.9) 100%);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.05),
    inset 0 0 0 1px rgba(255, 255, 255, 0.9),
    inset 0 1.5px 1px rgba(255, 255, 255, 1),
    inset 0 -1.5px 2px rgba(255, 255, 255, 0.55),
    0 8px 14px -10px rgba(15, 23, 42, 0.32),
    0 2px 4px -3px rgba(15, 23, 42, 0.2);
  transition:
    transform var(--up) var(--ease),
    box-shadow var(--up) var(--ease),
    color var(--up) var(--ease);
}

.social svg { width: 16px; height: 16px; fill: currentColor; }

/* Pressed: the key seats against the page and its shadow collapses. */
.social:hover,
.social:focus-visible {
  transition-duration: var(--down);
  transform: translateY(3px) scale(0.97);
  color: #5f6880;
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.05),
    inset 0 0 0 1px rgba(255, 255, 255, 0.7),
    inset 0 1px 1px rgba(255, 255, 255, 0.85),
    0 2px 4px -3px rgba(15, 23, 42, 0.3),
    0 1px 1px -1px rgba(15, 23, 42, 0.18);
}

/* ---------- Footer ---------- */
.footer {
  margin-top: 56px;
  text-align: center;
}

.footer__note {
  margin: 0;
  color: var(--faint);
  font-size: 13px;
}

/* ---------- Focus ---------- */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ---------- Project stack ---------- */
/* The stack is a frosted pane lying on the sky, and the keys lie on it.

   It exists because the key face and a cloud are both near white, so a key
   passing over a cloud had nothing to sit against and its edges dissolved.
   The pane fixes that twice over: the backdrop blur destroys the cloud
   detail underneath it, and the tint holds the region to one even value so
   a key is never half on a cloud and half off it.

   The tint is a pale blue white. Over the blurred sky it composites to
   about #d8ecf9 against a key face of #eef2f8: 10 values of mean
   separation and a luminance ratio of 1.08, where the deeper tint it
   replaced sat at #a9d2ed and 37 values. That is thin, and it is a
   deliberate trade. At this value the key is no longer separated by its
   colour; it is separated by its drop shadow, its white ring and its
   bevel, and the pane's job has narrowed to holding the region to one even
   value so a key is never half on a cloud and half off it. The blur went
   up with the whiteness rather than staying put, because it is carrying
   more of the work than it used to. Any whiter and there is nothing left
   to take away.

   The horizontal margin cancels the padding, so the pane grows outward and
   the keys keep the full column width. Without it the keys would narrow by
   the padding on both sides, and since their type sizes are container
   units against the key, every label would shrink with them. */
.stack {
  display: flex;
  flex-direction: column;
  gap: 24px;

  padding: 20px;
  margin-inline: -20px;
  /* Concentric with the 26px key radius across 20px of padding. */
  border-radius: 46px;
  background: rgba(232, 244, 252, 0.66);
  -webkit-backdrop-filter: blur(22px) saturate(105%);
  backdrop-filter: blur(22px) saturate(105%);
  /* The ring is a shade darker than the pane rather than lighter now. On
     the deeper tint a white ring described the edge; on a white pane a
     white ring is invisible and the pane has no edge at all. */
  box-shadow:
    inset 0 0 0 1px rgba(120, 163, 196, 0.32),
    inset 0 1px 1px rgba(255, 255, 255, 0.85),
    0 20px 40px -30px rgba(15, 23, 42, 0.5);
}

/* Without a backdrop filter the pane has no frosting, only its tint, and
   at 42 percent the clouds still read straight through it. It has to carry
   the separation on its own colour instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .stack { background: rgba(226, 241, 251, 0.93); }
}

.project { display: block; }

/* The key is a slab of glass. Three things sell it: light raking across the
   top face, bevelled walls at the edge, and a layered shadow holding it
   above the page. Every key is the same slab: no project colours the face,
   because a colour sampled off a client site is dictated by that site rather
   than chosen for this page, and a stack of them goes monotone the moment
   two dark ones land in a row. Each project's colour arrives through its own
   logo instead, which is the only place it was ever true. */
.project__key {
  position: relative;
  /* A text two thirds and a logo third, with the hairline sitting in the
     gap between them rather than being a column of its own. */
  display: grid;
  grid-template-columns: 2fr 1fr;
  /* Both columns run the full inner height so the hairline between them is
     the same length on every card, inset top and bottom by the padding. */
  align-items: stretch;
  aspect-ratio: 21 / 9;
  padding: 24px 28px;
  /* The label sizes off the width of the key, not the viewport, so it holds
     its proportions at any column width. */
  container-type: inline-size;
  border-radius: var(--radius);
  overflow: hidden;
  isolation: isolate;
  background: var(--key);
  color: var(--key-ink);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.05),
    0 28px 46px -26px rgba(15, 23, 42, 0.4),
    0 9px 18px -14px rgba(15, 23, 42, 0.28),
    0 2px 4px -2px rgba(15, 23, 42, 0.14);
  transition:
    transform var(--up) var(--ease),
    box-shadow var(--up) var(--ease);
}

/* Right third. The logo is contained rather than cropped, so badges of any
   proportion sit on the same optical centre. */
.project__badge {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  padding-left: 4cqw;
}

.project__logo {
  width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Left two thirds, printed on the panel and read through the glass. Sits
   below both glass layers, never over them. */
.project__label {
  position: relative;
  z-index: 1;
  /* The hairline is a border on the text column, so it lands exactly on the
     two thirds line and needs no element of its own. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-right: 1px solid var(--key-rule);
  padding-right: 5.2cqw;
  text-align: left;
}

/* Set in caps because both brands set their own name that way, and because
   caps at this size are what carry the weight of the button. */
.project__name {
  display: block;
  font-size: 28px;
  font-size: clamp(19px, 6.4cqw, 33px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.008em;
  text-transform: uppercase;
  text-wrap: pretty;
}

/* Stand-in for a real logo. Fills the badge cell the way a logo would. */
.project__mark {
  width: 62%;
  max-height: 100%;
  fill: currentColor;
}

/* Deliberately the opposite of the name: small, sentence case, and quiet.
   Held back with its own colour rather than with opacity, because opacity
   over the key face would let the glass layers above it show through the
   type and soften the letterforms. */
.project__sub {
  display: block;
  margin: 0.7em 0 0;
  font-size: 14px;
  font-size: clamp(12px, 2.8cqw, 14.5px);
  font-weight: 450;
  line-height: 1.42;
  letter-spacing: 0;
  text-transform: none;
  color: var(--key-sub);
  text-wrap: pretty;
}

/* The destination, so the visitor knows where the button leads before
   pressing it. Quieter than the subtitle and set in mono, because it is a
   machine address rather than prose. Also on its own colour, for the same
   reason the subtitle is. */
.project__host {
  display: block;
  margin: 0.9em 0 0;
  font-family: ui-monospace, 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
  font-size: 11px;
  font-size: clamp(9.5px, 2.1cqw, 11.5px);
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--key-faint);
}

/* The face: light caught by the top surface of the slab. */
.project__key::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  pointer-events: none;
  background:
    linear-gradient(168deg, rgba(255, 255, 255, 0.38) 0%, rgba(255, 255, 255, 0.09) 33%, rgba(255, 255, 255, 0) 54%),
    linear-gradient(to top, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 28%);
  transition: opacity var(--up) var(--ease);
}

/* The walls: the thickness of the glass, read as bevels. */
.project__key::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  pointer-events: none;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.72),
    inset 0 4px 6px -6px rgba(255, 255, 255, 1),
    inset 0 -4px 6px -6px rgba(255, 255, 255, 0.5),
    inset 5px 0 8px -6px rgba(255, 255, 255, 0.85),
    inset -5px 0 8px -6px rgba(255, 255, 255, 0.85),
    inset 0 -10px 14px -6px rgba(20, 30, 60, 0.28);
  transition: box-shadow var(--up) var(--ease);
}

/* ---------- The press ---------- */
/* Pressed means the slab has travelled down and is now flat against the
   page, so its shadow collapses to a contact shadow and its bevels soften.
   The face and everything printed on it do not change in any way.

   Every pressed rule overrides the duration to --down and inherits --up from
   the resting rule, which is what makes the press quick and the release
   slow. The scale is as important as the travel: shrinking the slab very
   slightly means the motion eases in two dimensions instead of one, and
   nothing that moves has a hard edge. */

.project:hover .project__key,
.project:focus-visible .project__key,
.project.is-pressed .project__key {
  transition-duration: var(--down);
  transform: translateY(6px) scale(0.988);
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.05),
    0 6px 12px -10px rgba(15, 23, 42, 0.45),
    0 2px 4px -3px rgba(15, 23, 42, 0.28),
    0 1px 1px -1px rgba(15, 23, 42, 0.18);
}

.project:hover .project__key::before,
.project:focus-visible .project__key::before,
.project.is-pressed .project__key::before {
  transition-duration: var(--down);
  opacity: 0.7;
}

.project:hover .project__key::after,
.project:focus-visible .project__key::after,
.project.is-pressed .project__key::after {
  transition-duration: var(--down);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.55),
    inset 0 3px 5px -6px rgba(255, 255, 255, 0.85),
    inset 0 -3px 5px -6px rgba(255, 255, 255, 0.35),
    inset 0 -7px 10px -6px rgba(20, 30, 60, 0.24);
}

.social.is-pressed {
  transition-duration: var(--down);
  transform: translateY(3px) scale(0.97);
  color: #5f6880;
  box-shadow:
    0 0 0 1px rgba(15, 23, 42, 0.05),
    inset 0 0 0 1px rgba(255, 255, 255, 0.7),
    inset 0 1px 1px rgba(255, 255, 255, 0.85),
    0 2px 4px -3px rgba(15, 23, 42, 0.3),
    0 1px 1px -1px rgba(15, 23, 42, 0.18);
}

/* The focus ring belongs to the whole link, not just the key. */
.project:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
  border-radius: 8px;
}

/* ---------- Reduced motion ---------- */
/* The sky is the one thing on the page that moves on its own, so it stops
   here and the clouds stay where they are. The press still happens,
   because it is a direct response to input, but it changes instantly. */
@media (prefers-reduced-motion: reduce) {
  .sky__layer { animation: none; }

  .project__key,
  .project__key::before,
  .project__key::after,
  .social {
    transition-duration: 1ms;
  }
}

/* ---------- Small screens ---------- */
@media (max-width: 480px) {
  .page { padding: 48px 16px 72px; }

  .header { margin-bottom: 40px; }
  .header__avatar { width: 64px; height: 64px; }
  .header__name { font-size: 24px; }
  .header__tagline { font-size: 14px; }

  /* The page padding drops to 16px here, so the pane can only bleed 12px
     before it runs off the screen. Radius stays concentric with the 20px
     key across that 12px. */
  .stack {
    gap: 18px;
    padding: 12px;
    margin-inline: -12px;
    border-radius: 32px;
  }

  .project__key {
    aspect-ratio: 16 / 10;
    border-radius: 20px;
    padding: 18px 20px;
  }
}
