/* ===========================================================================
   Canvas: the design system for every page Arsalan owns.
   Spec lives at personal-development/DESIGN-SYSTEM.md.

   The idea in one line: the page is dressed as the instrument it was built
   with. A ruler down the top edge, selection handles on the headline, a cursor
   with your name on it, a clock that really ticks. Every readout on this page
   is true, which is the rule that keeps the conceit from turning into costume.

   Two themes, and the dark one is night sky rather than an inverted document,
   because the light one is a day sky and the honest opposite of a day is a
   night. Colours are re-picked per theme, never flipped: a coral that sings on
   pale blue goes muddy on navy.
   =========================================================================== */

/* --------------------------------------------------------------- tokens -- */

:root,
:root[data-theme="light"] {
  color-scheme: light;

  --ink:    #15202B;
  --body:   #4B6274;
  --faint:  #8FA9BC;
  --accent: #E8552B;
  --select: #2B8FEA;

  --card:  #FFFFFF;
  --panel: #EDF1F6;
  --edge:  rgba(21, 32, 43, 0.13);

  --lift:   0 20px 50px -32px rgba(20, 19, 16, 0.32);
  --lift-2: 0 34px 64px -38px rgba(21, 32, 43, 0.5);

  /* Glass. Three parts, and all three are needed or it just looks washed out:
     a translucent fill, a blur that also lifts saturation so the sky behind
     keeps its colour instead of going grey, and a one-pixel highlight along
     the top edge where the light would catch a real pane.
     Opacity is the readability dial. Below about 60% the body copy starts
     fighting the clouds, so 68% is as far as this goes. */
  --glass:      color-mix(in srgb, var(--card) 68%, transparent);
  --glass-thin: color-mix(in srgb, var(--card) 55%, transparent);
  --glass-blur: saturate(180%) blur(20px);
  --glass-top:  inset 0 1px 0 rgba(255, 255, 255, 0.6);

  /* The sky. Three fixed layers: a sun glow top right, a cool wash bottom
     left, and a vertical ramp that lightens toward the horizon. Fixed, so it
     holds still while content scrolls over it. That is the whole trick. */
  --sky:
    radial-gradient(130% 95% at 82% -18%, rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0) 42%),
    radial-gradient(120% 80% at 8% 118%, rgba(198, 231, 255, 0.7) 0%, rgba(198, 231, 255, 0) 54%),
    linear-gradient(#4E9BD9 0%, #6FB2E6 42%, #97CBEF 78%, #B9DEF6 100%);

  --cloud: rgba(255, 255, 255, 0.9);
  --stars: 0;

  --mono:    "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans:    "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;
  /* Archivo, run slightly wide on the width axis. A signage grotesque rather
     than an editorial one: it reads as measured and built rather than as
     styled, which is the right register for an engineer's page. Variable, so
     the width comes free rather than as a second file. */
  --display: "Archivo", var(--sans);

  --r-card: 18px;
  --gutter: clamp(1rem, 4vw, 2.75rem);
  --col:    78rem;
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --ink:    #EAF1F7;
  --body:   #9FB6C6;
  --faint:  #6C879A;
  --accent: #FF6A3D;
  --select: #4FA6F0;

  --card:  #14222F;
  --panel: #1B2C3B;
  --edge:  rgba(255, 255, 255, 0.11);

  --lift:   0 20px 50px -30px rgba(0, 0, 0, 0.75);
  --lift-2: 0 34px 64px -34px rgba(0, 0, 0, 0.85);

  /* Glass at night. The fill is a touch more opaque because the night sky is
     darker than the panel, so light text needs the surface to hold its own,
     and the top highlight drops to a hint: a bright white line on a dark pane
     reads as a border rather than as light. */
  --glass:      color-mix(in srgb, var(--card) 72%, transparent);
  --glass-thin: color-mix(in srgb, var(--card) 60%, transparent);
  --glass-blur: saturate(170%) blur(20px);
  --glass-top:  inset 0 1px 0 rgba(255, 255, 255, 0.09);

  /* The same three layers after sunset. The glow stays top right because the
     sun has only just gone, and the ramp still lightens downward, which is
     what a horizon does at night too. */
  --sky:
    radial-gradient(130% 95% at 82% -18%, rgba(120, 160, 210, 0.28) 0%, rgba(120, 160, 210, 0) 44%),
    radial-gradient(120% 80% at 8% 118%, rgba(38, 70, 104, 0.75) 0%, rgba(38, 70, 104, 0) 56%),
    linear-gradient(#070E17 0%, #0B1622 44%, #12212F 76%, #1A2E3F 100%);

  --cloud: rgba(150, 178, 205, 0.15);
  --stars: 1;
}

/* ------------------------------------------------------------- skeleton -- */

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

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

body {
  margin: 0;
  min-height: 100svh;
  background: var(--sky) fixed;
  color: var(--ink);
  font: 500 1.02rem/1.62 var(--sans);
  overflow-x: clip;
  /* The ruler is pinned to the top edge and the pill chrome floats under it,
     so the document has to start below both. */
  padding-top: 6.2rem;
}

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

::selection { background: color-mix(in srgb, var(--select) 30%, transparent); }

:where(a, button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--select);
  outline-offset: 3px;
  border-radius: 6px;
}

.sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --------------------------------------------------------------- clouds -- */
/* Pure CSS, no images. Each cloud is a blurred ellipse with two more lumps on
   its pseudo-elements, drifting across over minutes. Slow enough to read as
   air rather than as an animation asking to be looked at. */

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

.cloud {
  position: absolute;
  background: var(--cloud);
  border-radius: 999px;
  filter: blur(22px);
  will-change: transform;
  animation: drift linear infinite;
}

.cloud::before,
.cloud::after {
  content: "";
  position: absolute;
  background: inherit;
  border-radius: inherit;
}

.cloud::before { width: 62%; height: 190%; left: 12%; top: -62%; }
.cloud::after  { width: 44%; height: 150%; right: 10%; top: -36%; }

/* Two depths. The near ones are bigger, faster and more opaque. */
.cloud.c1 { width: 320px; height: 62px; top: 12%; animation-duration: 190s; opacity: .85; }
.cloud.c2 { width: 460px; height: 84px; top: 33%; animation-duration: 260s; opacity: .70; animation-delay: -60s; }
.cloud.c3 { width: 240px; height: 48px; top: 56%; animation-duration: 220s; opacity: .60; animation-delay: -120s; }
.cloud.c4 { width: 540px; height: 96px; top: 73%; animation-duration: 320s; opacity: .50; animation-delay: -30s; }
.cloud.c5 { width: 300px; height: 56px; top: 89%; animation-duration: 240s; opacity: .65; animation-delay: -170s; }

@keyframes drift {
  from { transform: translateX(-45vw); }
  to   { transform: translateX(135vw); }
}

/* Stars, dark theme only. A tiled radial pattern rather than hundreds of
   elements, so the browser paints one layer instead of scheduling three
   hundred. */
.sky-layer::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: var(--stars);
  transition: opacity .5s linear;
  background-image:
    radial-gradient(1.4px 1.4px at 18% 22%, rgba(255,255,255,.9), transparent 60%),
    radial-gradient(1.1px 1.1px at 62% 12%, rgba(255,255,255,.7), transparent 60%),
    radial-gradient(1.6px 1.6px at 84% 46%, rgba(255,255,255,.85), transparent 60%),
    radial-gradient(1.1px 1.1px at 34% 66%, rgba(255,255,255,.6), transparent 60%),
    radial-gradient(1.3px 1.3px at 8% 84%,  rgba(255,255,255,.75), transparent 60%),
    radial-gradient(1px 1px at 72% 78%,     rgba(255,255,255,.55), transparent 60%);
  background-size: 420px 380px;
}

/* ---------------------------------------------------------------- ruler -- */
/* The strongest single signal in the system, and it costs almost nothing.
   Ticks every 10px and a taller one every 100, drawn with two repeating
   gradients rather than a thousand elements. */

.ruler {
  position: fixed;
  inset: 0 0 auto 0;
  height: 28px;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: 0 .7rem;
  background: color-mix(in srgb, var(--card) 82%, transparent);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--edge);
  font: 700 10px/1 var(--mono);
  color: var(--faint);
}

.ruler-brand {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  color: var(--ink);
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}

.ruler-mark {
  width: 15px; height: 15px;
  border-radius: 4px;
  background: var(--ink);
  color: var(--card);
  display: grid; place-items: center;
  font-size: 8px;
  letter-spacing: 0;
}

/* The measured strip: short ticks every 10px, tall ticks every 100px. */
.ruler-scale {
  position: relative;
  flex: 1;
  align-self: stretch;
  min-width: 0;
  opacity: .85;
}

.ruler-scale::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 11px;
  background-image:
    repeating-linear-gradient(90deg, var(--edge) 0 1px, transparent 1px 10px),
    repeating-linear-gradient(90deg, var(--faint) 0 1px, transparent 1px 100px);
  background-size: 100% 6px, 100% 11px;
  background-position: 0 100%, 0 100%;
  background-repeat: repeat-x;
}

/* Coral fill showing how far down the page you are, painted onto the ruler
   itself so the measurement and the thing it measures are one object. */
.ruler-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  border-right: 1px solid var(--accent);
}

.ruler-live {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  white-space: nowrap;
  letter-spacing: .08em;
}

.ruler-pct {
  min-width: 36px;
  text-align: center;
  padding: 3px 5px;
  border-radius: 4px;
  background: var(--accent);
  color: #fff;
  letter-spacing: .04em;
}


/* --------------------------------------------------------------- chrome -- */
/* Three floating pills over the sky: status left, nav centre, actions right. */

.chrome {
  position: fixed;
  top: 40px;
  left: 0; right: 0;
  z-index: 55;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  padding: 0 var(--gutter);
  pointer-events: none;
}

.chrome > * { pointer-events: auto; }

.pill {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .55rem .9rem;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--edge);
  border-radius: 999px;
  box-shadow: var(--lift), var(--glass-top);
  font: 600 .82rem/1 var(--sans);
  color: var(--ink);
  white-space: nowrap;
}

.pill-status {
  font: 700 .66rem/1 var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--body);
}

.pill-nav { gap: .12rem; padding: .38rem .42rem; }

.pill-nav a {
  padding: .42rem .78rem;
  border-radius: 999px;
  text-decoration: none;
  color: var(--body);
  transition: background .18s, color .18s;
}

.pill-nav a:hover { background: var(--panel); color: var(--ink); }

.pill-nav a.is-cta { background: var(--ink); color: var(--card); }
.pill-nav a.is-cta:hover { background: var(--accent); color: #fff; }

.pill-actions { gap: .25rem; padding: .38rem .42rem; }

.iconbtn {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--body);
  cursor: pointer;
  transition: background .18s, color .18s;
}

.iconbtn:hover { background: var(--panel); color: var(--ink); }
.iconbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.iconbtn.social svg { fill: currentColor; stroke: none; }

.langbtn {
  border: 0;
  background: transparent;
  padding: .42rem .7rem;
  border-radius: 999px;
  font: 700 .66rem/1 var(--mono);
  letter-spacing: .08em;
  color: var(--body);
  cursor: pointer;
  transition: background .18s, color .18s;
}

.langbtn:hover { background: var(--panel); color: var(--ink); }

/* Theme icons swap with the theme. */
:root[data-theme="dark"]  .i-sun  { display: block; }
:root[data-theme="dark"]  .i-moon { display: none; }
:root[data-theme="light"] .i-sun  { display: none; }
:root[data-theme="light"] .i-moon { display: block; }

/* -------------------------------------------------------------- layout -- */

main { position: relative; z-index: 1; }

.wrap { width: min(100% - var(--gutter) * 2, var(--col)); margin-inline: auto; }

section { padding-block: clamp(3rem, 8vw, 6rem); }

.eyebrow {
  margin: 0 0 .9rem;
  font: 700 .66rem/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 800;
  font-variation-settings: "wdth" 112;
  letter-spacing: -.025em;
  margin: 0;
}

h2 { font-size: clamp(2rem, 5.2vw, 3.4rem); line-height: 1; margin-bottom: .9rem; }
h3 { font-size: 1.16rem; line-height: 1.2; letter-spacing: -.02em; }

.lede {
  max-width: 48ch;
  margin: 1.1rem 0 0;
  font-size: clamp(1.02rem, 1.6vw, 1.16rem);
  color: var(--body);
}

.section-lede { max-width: 56ch; margin: 0 0 2.4rem; color: var(--body); }

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

.hero { padding-block: clamp(2rem, 6vw, 4rem) clamp(2.5rem, 7vw, 5rem); text-align: center; }
.hero .lede { margin-inline: auto; }

/* The badge above the headline carries a real number, not a slogan. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  padding: .6rem 1rem;
  margin-bottom: clamp(1.5rem, 4.5vw, 2.75rem);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--edge);
  border-radius: 12px;
  box-shadow: var(--lift), var(--glass-top);
  font: 600 .88rem/1 var(--sans);
  color: var(--ink);
}

.badge b { font-weight: 800; }
.badge .m { font: 700 .68rem/1 var(--mono); letter-spacing: .08em; color: var(--faint); }

/* Selection box: dashed outline, four handles, a name tag. The only fiction
   on the page, and an obvious one, which is why it is allowed. */
.selection { position: relative; display: inline-block; padding: .55rem 1.3rem; }

.selection::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px dashed var(--select);
  pointer-events: none;
}

.handle {
  position: absolute;
  width: 7px; height: 7px;
  background: var(--card);
  border: 1.5px solid var(--select);
  border-radius: 2px;
}

.handle.tl { top: -4px; left: -4px; }
.handle.tr { top: -4px; right: -4px; }
.handle.bl { bottom: -4px; left: -4px; }
.handle.br { bottom: -4px; right: -4px; }

.sel-tag {
  position: absolute;
  top: -19px; left: -1px;
  padding: 2px 6px;
  border-radius: 3px 3px 0 0;
  background: var(--select);
  color: #fff;
  font: 700 9px/1.5 var(--mono);
  letter-spacing: .06em;
  white-space: nowrap;
}

.title { font-size: clamp(2.5rem, 8.2vw, 6rem); line-height: .92; }
.title .accent { color: var(--accent); }

/* German sets wider than English at the same size, because German compounds
   are longer and there is no way around that. Measured on this headline:
   the longest English line needs 1203px against 1248px available, the German
   equivalent needs 1513px, so it has to come down by about a sixth or it
   wraps to four lines and swamps the hero. A language-specific display size
   is the normal fix and is cheaper than writing shorter German that says
   less. */
:root[lang="de"] .title { font-size: clamp(2.1rem, 6.4vw, 4.55rem); }

.cta { display: flex; flex-wrap: wrap; gap: .7rem; justify-content: center; margin-top: 1.9rem; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .82rem 1.35rem;
  border-radius: 999px;
  border: 1px solid var(--edge);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  color: var(--ink);
  font: 700 .92rem/1 var(--sans);
  text-decoration: none;
  box-shadow: var(--lift), var(--glass-top);
  transition: transform .18s, box-shadow .18s, background .18s;
}

.btn:hover { transform: translateY(-2px); box-shadow: var(--lift-2); }
.btn svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.btn-primary { background: var(--accent); border-color: transparent; color: #fff; }

/* ---------------------------------------------------------------- bento -- */

.bento { display: grid; gap: 1rem; grid-template-columns: repeat(12, 1fr); }

.card {
  position: relative;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--edge);
  border-radius: var(--r-card);
  box-shadow: var(--lift), var(--glass-top);
  padding: 1.5rem;
  transition: transform .2s, box-shadow .2s;
}

a.card { text-decoration: none; color: inherit; display: block; }
a.card:hover { transform: translateY(-2px); box-shadow: var(--lift-2); }

/* The mono label in the corner of a card. */
.label {
  font: 700 .62rem/1 var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--faint);
}

.card > .label:first-child { display: block; margin-bottom: 1rem; }

/* A display-size line inside a card, for the one statement that is worth
   saying loudly without being the page headline. */
.card-lead {
  margin: -.35rem 0 1.1rem;
  font: 800 clamp(1.5rem, 2.6vw, 2.05rem)/1.05 var(--display);
  font-variation-settings: "wdth" 108;
  letter-spacing: -.025em;
  color: var(--ink);
}

.card-lead .accent { color: var(--accent); }
.label-tr { position: absolute; top: 1.35rem; right: 1.4rem; }

/* One inverted card per screen, for rhythm.

   The dot grid is texture, not content, so it sits at 12% and behind
   everything. At full currentColor it prints dots straight through the
   paragraph, which was the first thing this card got wrong. */
.card-ink {
  background-color: var(--ink);
  color: var(--card);
  border-color: transparent;
  background-image: radial-gradient(color-mix(in srgb, currentColor 12%, transparent) .9px, transparent .9px);
  background-size: 16px 16px;
  background-position: 8px 8px;
}

.card-ink > * { position: relative; }

:root[data-theme="dark"] .card-ink { background-color: #050B12; color: #EAF1F7; }
.card-ink .label { color: color-mix(in srgb, currentColor 55%, transparent); }
.card-ink p { color: color-mix(in srgb, currentColor 78%, transparent); }

/* Column spans. The grid is twelve wide and these are the only spans used. */
.s3  { grid-column: span 3; }
.s4  { grid-column: span 4; }
.s5  { grid-column: span 5; }
.s6  { grid-column: span 6; }
.s7  { grid-column: span 7; }
.s8  { grid-column: span 8; }
.s12 { grid-column: span 12; }

/* ----------------------------------------------------------------- stat -- */

.stat b {
  display: block;
  font: 800 clamp(2.1rem, 4.2vw, 3.1rem)/1 var(--display);
  letter-spacing: -.04em;
}

.stat b sup { color: var(--accent); font-size: .48em; vertical-align: super; margin-left: .04em; }
.stat .label { display: block; margin-top: .5rem; }

.statlist { display: grid; gap: 0; }
.statlist > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding-block: .95rem;
  border-bottom: 1px solid var(--edge);
}
.statlist > div:last-child { border-bottom: 0; padding-bottom: 0; }
.statlist > div:first-child { padding-top: 0; }
.statlist .label { text-align: right; }

/* ---------------------------------------------------------------- chips -- */

.chips { display: flex; flex-wrap: wrap; gap: .45rem; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .42rem .8rem;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: var(--glass-thin);
  font: 600 .8rem/1 var(--sans);
  color: var(--ink);
  white-space: nowrap;
}

.chip-mono { font: 700 .62rem/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--body); }

.card-ink .chip {
  background: color-mix(in srgb, currentColor 8%, transparent);
  border-color: color-mix(in srgb, currentColor 18%, transparent);
  color: inherit;
}

/* ----------------------------------------------------------- file label -- */
/* A language-coloured square and a filename, above whatever it labels. The
   colours are the ones GitHub uses, so they are already familiar. */

.file {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  margin-bottom: .55rem;
  font: 700 .68rem/1 var(--mono);
  letter-spacing: .03em;
  color: var(--body);
}

.file i { width: 9px; height: 9px; border-radius: 2px; background: var(--faint); flex: none; }

/* GitHub's own language colours, so the dots agree with the repo pages. */
.f-sol  i { background: #AA6746; }
.f-ts   i { background: #3178C6; }
.f-py   i { background: #3572A5; }
.f-java i { background: #B07219; }
.f-js   i { background: #F1E05A; }
.f-md   i { background: #8AA6B8; }

/* -------------------------------------------------------- browser frame -- */
/* A project screenshot inside fake browser chrome. The work is websites, so it
   is framed as one. */

.frame {
  border: 1px solid var(--edge);
  border-radius: 14px;
  overflow: hidden;
  background: var(--card);
}

.frame-bar {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .7rem;
  border-bottom: 1px solid var(--edge);
  background: var(--panel);
}

.lights { display: inline-flex; gap: .3rem; flex: none; }
.lights i { width: 8px; height: 8px; border-radius: 50%; }
.lights i:nth-child(1) { background: #ED6A5E; }
.lights i:nth-child(2) { background: #F4BF4F; }
.lights i:nth-child(3) { background: #61C554; }

.frame-url {
  flex: 1;
  text-align: center;
  font: 700 .6rem/1 var(--mono);
  color: var(--faint);
  letter-spacing: .03em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Fixed aspect so the grid never reflows as images load, and the shot travels
   inside the frame on hover rather than the card growing.

   The frame is deliberately wider than the screenshots (2:1 against their 1.79)
   so there is something to travel through. The travel is done with
   object-position rather than a transform, because a transform needs a distance
   in pixels and every screenshot has a slightly different aspect: 850x475 here,
   850x448 there. `top` to `bottom` means "align whichever edge", so it lands
   exactly right for each one without a per-image number. */
.frame-view { position: relative; aspect-ratio: 2 / 1; overflow: hidden; background: var(--panel); }

.frame-view img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: 50% 0%;
  transition: object-position 3s cubic-bezier(.4, 0, .2, 1);
}

a.card:hover .frame-view img { object-position: 50% 100%; }

.frame-note {
  position: absolute;
  left: 50%; bottom: .6rem;
  transform: translateX(-50%);
  padding: .3rem .65rem;
  border-radius: 999px;
  background: color-mix(in srgb, #0B131B 82%, transparent);
  color: #fff;
  font: 700 .56rem/1 var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity .25s;
  pointer-events: none;
}

a.card:hover .frame-note { opacity: 1; }

/* ------------------------------------------------------------ work grid -- */

.work { display: grid; gap: clamp(1.25rem, 3vw, 1.75rem); grid-template-columns: repeat(2, minmax(0, 1fr)); }

.work .card { padding: 1rem; }
.work-head { display: flex; align-items: baseline; gap: .6rem; margin: .95rem .4rem 0; }
.work-head h3 { font-size: clamp(1.15rem, 2vw, 1.5rem); }
.work-head .year { margin-left: auto; font: 700 .66rem/1 var(--mono); color: var(--faint); }
.work p { margin: .45rem .4rem .8rem; color: var(--body); font-size: .93rem; }
.work .chips { margin: 0 .4rem .3rem; }

/* ---------------------------------------------------------- sticky note -- */

.note {
  position: relative;
  align-self: start;
  padding: 1.05rem 1.15rem;
  background: #FDF3D4;
  color: #4A3B12;
  border-radius: 3px;
  box-shadow: var(--lift);
  transform: rotate(-1.4deg);
  font-size: .93rem;
}

:root[data-theme="dark"] .note { background: #F1E2B6; }

.note::after {
  content: "";
  position: absolute;
  right: 0; bottom: 0;
  border-width: 0 0 18px 18px;
  border-style: solid;
  border-color: transparent transparent rgba(0, 0, 0, .13) transparent;
}

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

.foot {
  position: relative;
  margin-top: clamp(3rem, 8vw, 6rem);
  padding: clamp(2.25rem, 5vw, 3.5rem) 0 1rem;
  background-color: var(--ink);
  color: var(--card);
  border-top-left-radius: 22px;
  border-top-right-radius: 22px;
  background-image: radial-gradient(color-mix(in srgb, currentColor 10%, transparent) .9px, transparent .9px);
  background-size: 18px 18px;
  overflow: hidden;
}

.foot > * { position: relative; }

:root[data-theme="dark"] .foot { background-color: #050B12; color: #EAF1F7; }

.foot .label { color: color-mix(in srgb, currentColor 50%, transparent); }

.foot-top {
  display: grid;
  gap: 2.5rem;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  align-items: start;
}

.foot-cta {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  margin: .7rem 0 1.3rem;
  font: 800 clamp(1.35rem, 3.4vw, 2.4rem)/1.05 var(--display);
  letter-spacing: -.03em;
  color: inherit;
  text-decoration: none;
  word-break: break-word;
}

.foot-arrow { width: .62em; height: .62em; stroke: var(--accent); stroke-width: 2.4; fill: none; flex: none; }
.foot-cta:hover { color: var(--accent); }

/* The LinkedIn mark, in LinkedIn's own blue rather than the page accent. A
   brand mark recoloured to match a palette stops being recognisable, and being
   recognisable at a glance is the entire reason it is here. */
.foot-brand {
  flex: none;
  display: grid;
  place-items: center;
  width: .92em;
  height: .92em;
  border-radius: .16em;
  background: #0A66C2;
}

.foot-brand svg { width: 74%; height: 74%; fill: #fff; stroke: none; }

/* Attribution. Quiet, but on the page rather than only in the repository. */
.foot-credit {
  margin: .75rem 0 0;
  font: 700 .62rem/1.6 var(--mono);
  letter-spacing: .06em;
  color: color-mix(in srgb, currentColor 42%, transparent);
}

.foot-credit a { color: color-mix(in srgb, currentColor 70%, transparent); }
.foot-credit a:hover { color: var(--accent); }

.foot-links { display: grid; gap: 1.5rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.foot-links h4 {
  margin: 0 0 .7rem;
  font: 700 .62rem/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: color-mix(in srgb, currentColor 50%, transparent);
}
.foot-links a { display: block; padding: .22rem 0; color: inherit; text-decoration: none; font-size: .92rem; opacity: .85; }
.foot-links a:hover { opacity: 1; color: var(--accent); }

/* The wordmark, cropped at the baseline. Sized in vw so it always fills the
   width, with the descenders running off the bottom edge on purpose. */
/* The wordmark fills the column edge to edge and is cropped at the baseline on
   purpose, so the descenders run off the bottom. It must not crop at the right
   edge, and it must not stop short of it either: both read as a mistake rather
   than as a decision.
 *
 * Sized against the container, not the viewport. A vw-based size looks right
 * at one window width and wrong at every other, because the column is capped
 * at 78rem while vw keeps growing: past about 1350px the two diverge, and the
 * clamp ceiling that stops the text overflowing on a wide screen is the same
 * ceiling that leaves a gap on the right. 1cqw is one percent of the column,
 * so the mark tracks the thing it is supposed to line up with and the
 * coefficient holds at every width.
 *
 * The vw version stays as a fallback for browsers without container queries,
 * tuned to be slightly small rather than slightly large: falling short is
 * untidy, overflowing loses a letter. */
.foot .wrap { container-type: inline-size; }

.foot-mark {
  margin: clamp(1.75rem, 5vw, 3.25rem) 0 0;
  font: 800 clamp(2rem, 10.6vw, 9.8rem)/.82 var(--display);
  font-variation-settings: "wdth" 100;
  letter-spacing: -.05em;
  white-space: nowrap;
  color: color-mix(in srgb, currentColor 92%, transparent);
  user-select: none;
}

@supports (font-size: 1cqw) {
  .foot-mark { font-size: 14.3cqw; }
}

.foot-mark span { color: var(--accent); }

.foot-bar {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem 1.4rem;
  justify-content: space-between;
  align-items: center;
  margin-top: 1.3rem;
  padding-top: 1rem;
  border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
  font: 700 .62rem/1.5 var(--mono);
  letter-spacing: .06em;
  color: color-mix(in srgb, currentColor 58%, transparent);
}

.foot-bar a { color: inherit; }
.foot-bar a:hover { color: var(--accent); }
.foot-bar span { display: inline-flex; align-items: center; gap: .4rem; }

/* --------------------------------------------------------------- cursor -- */
/* Follows the real pointer. Hidden on touch and under reduced motion, because
   a cursor label with no cursor under it is a lie. */

.cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: 70;
  pointer-events: none;
  display: none;
  align-items: flex-start;
  gap: 1px;
  will-change: transform;
  /* Hidden until the pointer has actually moved. Without this it sits at the
     origin on load with a label reading "you", parked over the wordmark,
     pointing at nothing. */
  opacity: 0;
  transition: opacity .2s linear;
}

.cursor.is-live { opacity: 1; }

.cursor svg { width: 15px; height: 15px; fill: var(--select); filter: drop-shadow(0 1px 2px rgba(0,0,0,.3)); }

.cursor b {
  margin-top: 9px;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--select);
  color: #fff;
  font: 700 9px/1.5 var(--mono);
  letter-spacing: .06em;
}

@media (hover: hover) and (pointer: fine) {
  .cursor { display: flex; }
}

/* --------------------------------------------------------------- to top -- */

.totop {
  position: fixed;
  right: clamp(1rem, 3vw, 2rem);
  bottom: clamp(1rem, 3vw, 2rem);
  z-index: 56;
  width: 42px; height: 42px;
  display: grid; place-items: center;
  border: 1px solid var(--edge);
  border-radius: 50%;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift), var(--glass-top);
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity .25s, transform .25s;
}

.totop.is-on { opacity: 1; transform: none; pointer-events: auto; }
.totop svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* --------------------------------------------------------------- reveal -- */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .5s cubic-bezier(.22,1,.36,1), transform .5s cubic-bezier(.22,1,.36,1);
}

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

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

@media (max-width: 62rem) {
  .bento > * { grid-column: span 6; }
  .s12 { grid-column: span 12; }
  .foot-top { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 48rem) {
  body { padding-top: 5.6rem; }
  .chrome { flex-wrap: wrap; justify-content: center; gap: .45rem; }
  /* The nav has to survive; the status pill is the first thing worth losing. */
  .pill-status { display: none; }
  .pill-nav { order: -1; max-width: 100%; overflow-x: auto; }
  .bento > * { grid-column: span 12; }
  .work { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 30rem) {
  .ruler-scale { display: none; }
  .pill-nav a { padding: .4rem .56rem; font-size: .78rem; }
}

/* ------------------------------------------------------- reduced motion -- */
/* The page has to be complete with every animation removed, so this stops the
   drift and lands every reveal in its final state rather than hiding it. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .cloud { animation: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
  .cursor { display: none; }
  .frame-view img,
  .btn, a.card, .totop { transition: none; }
}
