/* ==========================================================================
   safwan syed — horizontal scrolling portfolio

   The source design is a 1024.5 x 576 canvas. Every element is positioned in
   those design units and multiplied by --s, the scale that fits the canvas in
   the viewport. That reproduces the artwork exactly at any desktop size.
   Below --bp the layout switches to a reflowed column (see the media query).
   ========================================================================== */

:root {
  --design-w: 1024.5;
  --design-h: 576;

  /* Scale so the whole 16:9 canvas fits the viewport. */
  --s: min(100vw / var(--design-w), 100dvh / var(--design-h));

  --fs: calc(16.2 * var(--s));   /* body size, measured off the source */
  --lh: 1.2;

  /* Where the baseline sits below the top of a line-height:1 box in
     EB Garamond. Used to place type by its baseline, as the source does. */
  --bo: 0.84;

  --ink:    #385793;             /* exact slate blue from the reference */
  --cream:  #f7f1e1;             /* type + line art */
  --muted:  #bdbdbd;             /* inactive nav */
  --slate:  #385793;             /* form text, linkedin mark */

  --fish-w: 182.16;
  --fish-h: 63.72;

  --ease: cubic-bezier(.22, .61, .36, 1);
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--ink);
  color: var(--cream);
  font-family: "EB Garamond", Garamond, "Times New Roman", serif;
  font-size: var(--fs);
  line-height: var(--lh);
  font-feature-settings: "onum" 1;   /* old-style figures, as in the source */
  -webkit-font-smoothing: antialiased;
}

body { overflow: hidden; }

a { color: inherit; text-decoration: none; }

.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* ── the water ────────────────────────────────────────────────────────────
   A depth wash plus a fine grain, so the flat blue reads as water rather
   than as a swatch. The grain is one inline SVG turbulence tile, so there is
   no image to download; overlay blending keeps it from greying the colour. */

/*
   Grain and depth are painted as background layers on the body rather than as
   a blended overlay. A fixed element using mix-blend-mode has to composite
   against the animating fish layers, and Chrome clips that blend to the layer
   bounds — which showed up as a hard-edged rectangle across the page.

   The noise tile maps turbulence luminance onto *alpha* (the feColorMatrix
   row `1 0 0 0 -0.4`), so it lays down dark specks instead of a flat grey
   wash that would desaturate the blue.
*/
body {
  background-color: var(--ink);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 -0.4'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E"),
    radial-gradient(140% 95% at 50% -15%, rgba(255,255,255,.085), transparent 72%),
    radial-gradient(150% 120% at 50% 125%, rgba(0,0,0,.20),        transparent 78%);
  background-size: 220px 220px, cover, cover;
  background-repeat: repeat, no-repeat, no-repeat;
  background-attachment: fixed, fixed, fixed;
}

/* ── background fish ──────────────────────────────────────────────────── */

/*
   Always-on traffic: a handful of very faint fish cruising across, far too
   slowly to distract. The drifter span carries the long horizontal cruise and
   the img inside carries a short sway, so the two never fight over transform.
*/
#ambient {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  overflow: hidden;
}
#ambient .drifter {
  position: absolute;
  left: 0;
  top:   calc(var(--t) * 1%);
  width: calc(var(--w) * 1%);
  animation: cruise var(--dur) linear var(--d) infinite;
}
#ambient .drifter img {
  display: block;
  width: 100%;
  filter: grayscale(1) brightness(.72);
  opacity: var(--op);
  animation: sway var(--sway) ease-in-out var(--d) infinite;
}
@keyframes cruise {
  from { transform: translateX(calc(50vw - var(--dir) * 78vw)) scaleX(var(--dir)); }
  to   { transform: translateX(calc(50vw + var(--dir) * 78vw)) scaleX(var(--dir)); }
}
@keyframes sway {
  0%, 100% { transform: translateY(0)     rotate(0deg);    }
  50%      { transform: translateY(-2.5%) rotate(-1.4deg); }
}

/* ── everything suspended in water ────────────────────────────────────────
   A slow drift on every run of type. It uses the `translate` property rather
   than `transform`, because the centred lines already spend their transform
   on translateX(-50%) — the two compose instead of overwriting each other.
   script.js gives each element its own duration and phase so the page never
   bobs in unison. */

@keyframes drift {
  0%   { translate: 0       0;      }
  25%  { translate:  .05em -.09em;  }
  50%  { translate: 0      -.15em;  }
  75%  { translate: -.05em -.06em;  }
  100% { translate: 0       0;      }
}

.ln, .row > a, .row .ico, #nav a, .wordmark, .send {
  animation: drift var(--fdur, 7s) ease-in-out var(--fdel, 0s) infinite;
}

/* ── letters knocked about by the fish ────────────────────────────────────
   Text is only split into .ch spans for the duration of a transition, then
   put back verbatim — per-character spans lose kerning, which would shift
   the carefully placed resting layout. --dx/--dy are em, so the impulse
   scales with the type. */

.ch {
  display: inline-block;
  white-space: pre;
}
.ch.knock,
.ch.gather { will-change: transform, opacity; }

.ch.knock  { animation: knock  600ms cubic-bezier(.25, .7, .3, 1)  var(--d) both; }
.ch.gather { animation: gather 620ms cubic-bezier(.16, .84, .3, 1) var(--d) both; }

@keyframes knock {
  0%   { transform: none; opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) rotate(var(--r)); opacity: 0; }
}
@keyframes gather {
  0%   { transform: translate(var(--dx), var(--dy)) rotate(var(--r)); opacity: 0; }
  60%  { opacity: 1; }
  100% { transform: none; opacity: 1; }
}

/* ── the school ───────────────────────────────────────────────────────── */

#school {
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  overflow: hidden;
}
/*
   script.js rebuilds this scatter on every panel change, giving each fish:
     --l/--t position (%)   --w width (%)   --d delay   --op peak opacity
     --dir   +1 drifts right, -1 drifts left
   --dir is set opposite to the direction you are travelling, so the school
   slides past the way scenery does when you are the one moving. The artwork
   faces right, so scaleX(var(--dir)) also turns each fish to face its heading.
   Inserting the elements is what starts the animation.
*/
#school img {
  position: absolute;
  left:  calc(var(--l) * 1%);
  top:   calc(var(--t) * 1%);
  width: calc(var(--w) * 1%);
  filter: grayscale(1) brightness(.78);
  opacity: 0;
  /* linear: a fish crossing open water holds its speed. Anything with ease
     in it makes them look like they are being dragged on elastic. */
  animation: pass var(--dur) linear var(--d) both;
}
/* One continuous sweep — only the opacity is keyframed, so the motion never
   stalls. (An earlier version pinned translateX to 0 across the middle of the
   timeline, which made them stop dead mid-screen and then jerk away.) */
@keyframes pass {
  0%   { transform: translateX(calc(var(--dir) * -42vw)) scaleX(var(--dir)); opacity: 0; }
  14%  { opacity: var(--op); }
  86%  { opacity: var(--op); }
  100% { transform: translateX(calc(var(--dir) *  42vw)) scaleX(var(--dir)); opacity: 0; }
}

/* ── the deck ─────────────────────────────────────────────────────────── */

#deck {
  position: relative;
  z-index: 10;
  display: flex;
  height: 100dvh;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
#deck::-webkit-scrollbar { display: none; }

.panel {
  position: relative;
  flex: 0 0 100vw;
  height: 100dvh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  place-items: center;
}

/* The design canvas, centred in each panel. Children position against it. */
.canvas {
  position: relative;
  width:  calc(var(--design-w) * var(--s));
  height: calc(var(--design-h) * var(--s));
}

/* Chrome that stays put while the deck scrolls underneath it. Centred with
   the identical grid rule the panels use, so coordinates line up exactly. */
#stage {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  pointer-events: none;
}
#stage a, #stage button { pointer-events: auto; }

/* ── positioned type ──────────────────────────────────────────────────── */

/*
   --by is the baseline in design units. With line-height:1 the EB Garamond
   baseline sits ~0.74em below the top of the line box, so we offset by that
   to land text exactly where the source has it.
   --x  = left edge      --cx = horizontal centre
*/
.ln {
  position: absolute;
  margin: 0;
  line-height: 1;
  white-space: nowrap;
  top: calc((var(--by) - 16.2 * var(--bo)) * var(--s));
}
.ln[style*="--x:"]  { left: calc(var(--x) * var(--s)); }
.ln[style*="--cx:"] { left: calc(var(--cx) * var(--s)); transform: translateX(-50%); }

.head { font-size: 1em; font-weight: 600; }

/* The panel title, matching the "skills" label in the source frame:
   same size and weight as body copy, sitting above the content. */
.panel-title { color: var(--cream); }

/* ── nav + wordmark ───────────────────────────────────────────────────── */

/*
   Nav and wordmark are positioned against #stage, i.e. the viewport, not the
   design canvas — so they stay in the real screen corners when the 16:9
   canvas is letterboxed on a wide or short window. The offsets are the
   source's own (11.4 and 13.5 design units) with a pixel floor so they never
   collapse onto the edge when --s gets small.
*/
#nav {
  position: absolute;
  left: max(12px, calc(11.4 * var(--s)));
  top:  max(10px, calc((26.6 - 16.2 * var(--bo)) * var(--s)));
  z-index: 40;
}
/* line-height:1 here so each li's strut matches the link box and the
   baselines land on the source's 19.49-unit rhythm exactly. */
#nav ul { margin: 0; padding: 0; list-style: none; line-height: 1; }
#nav li { height: calc(19.49 * var(--s)); }
#nav a {
  display: inline-block;
  line-height: 1;
  color: var(--muted);
  transition: color .35s var(--ease);
}
#nav a:hover,
#nav a:focus-visible { color: var(--cream); }
#nav a[aria-current="true"] { color: var(--cream); }

/* On the home panel nothing is dimmed yet — the menu sits at rest. */
body[data-panel="0"] #nav a { color: var(--cream); }
body.schooling .start {
  opacity: 0;
  pointer-events: none;
  animation: none;
}

.wordmark {
  position: absolute;
  right: max(12px, calc(13.5 * var(--s)));
  top:   max(10px, calc((26.6 - 16.2 * var(--bo)) * var(--s)));
  line-height: 1;
  z-index: 40;
  color: var(--cream);
}

/* ── the fish ─────────────────────────────────────────────────────────── */

/* Registered so the tilt can be interpolated rather than snapping. */
@property --tilt {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

#fish {
  position: absolute;
  z-index: 20;
  pointer-events: none;
  top: 50%;
  left: 0;
  width:  calc(var(--fish-w) * var(--s));
  height: calc(var(--fish-h) * var(--s));
  /* --fx (design-unit centre) and --tilt are driven from script.js */
  --fx: 490.1;
  --tilt: 0deg;
  transform:
    translate(calc(var(--fx) * var(--s) - 50%), -50%)
    rotate(var(--tilt));
  /* Tilt is driven per frame from the fish's own velocity, so it only needs
     enough smoothing to take the edge off — not a visible catch-up lag. */
  transition: --tilt .12s linear;
  will-change: transform;
}
#fish img { display: block; width: 100%; height: 100%; }

/* Two slow, mismatched cycles so the idle never reads as a metronome. */
#fish img { animation: bob 7.5s ease-in-out infinite; }
@keyframes bob {
  0%, 100% { transform: translateY(0)     rotate(0deg);   }
  28%      { transform: translateY(-2.6%) rotate(-.9deg); }
  62%      { transform: translateY(1.8%)  rotate(.7deg);  }
}

/* ── home ─────────────────────────────────────────────────────────────── */

.start {
  font: inherit;
  line-height: 1;      /* the `font` shorthand above resets it */
  color: var(--cream);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  opacity: 1;
}
.start:hover, .start:focus-visible { opacity: .72; }

/* ── skills ───────────────────────────────────────────────────────────── */

.cloud { margin: 0; padding: 0; list-style: none; }

.cert,
.certlink {
  text-decoration: underline;
  text-decoration-thickness: calc(.8 * var(--s));
  text-underline-offset: calc(2.5 * var(--s));
}
.certlink { transition: opacity .3s var(--ease); }
.certlink:hover, .certlink:focus-visible { opacity: .65; }

/* ── contact ──────────────────────────────────────────────────────────── */

.contacts { margin: 0; padding: 0; list-style: none; }

.row .ico {
  position: absolute;
  left: calc(var(--ix) * var(--s));
  top:  calc(var(--iy) * var(--s));
  width:  calc(30.7 * var(--s));
  height: calc(30.7 * var(--s));
}
.row .ico svg { width: 100%; height: 100%; display: block; }
/* All three marks read the same way: a cream disc with the blue mark on it. */
.row .ico .disc { fill: var(--cream); }
.row .ico .mark { fill: none; stroke: var(--slate); stroke-width: 1.7; stroke-linejoin: round; }
.row .ico .fill { fill: var(--slate); }

.row > a {
  position: absolute;
  left: calc(var(--tx) * var(--s));
  top:  calc((var(--by) - 16.2 * var(--bo)) * var(--s));
  line-height: 1;
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-thickness: calc(.8 * var(--s));
  text-underline-offset: calc(2.5 * var(--s));
  transition: opacity .3s var(--ease);
}
.row > a:hover, .row > a:focus-visible { opacity: .65; }

/* all three contact rows are underlined links */

/* form */
.field {
  position: absolute;
  left: calc(var(--x) * var(--s));
  top:  calc(var(--y) * var(--s));
  width:  calc(var(--w) * var(--s));
  height: calc(var(--h) * var(--s));
  display: block;
}
.field input,
.field textarea {
  font: inherit;
  width: 100%;
  height: 100%;
  display: block;
  background: var(--cream);
  color: var(--slate);
  border: 0;
  border-radius: 0;
  padding: 0 calc(3.9 * var(--s));
  resize: none;
}
.field textarea { padding: calc(4 * var(--s)) calc(3.9 * var(--s)); line-height: 1.35; }
.field input::placeholder,
.field textarea::placeholder { color: var(--slate); opacity: 1; }
.field input:focus-visible,
.field textarea:focus-visible { outline: calc(1.5 * var(--s)) solid var(--cream); outline-offset: calc(1.5 * var(--s)); }

.send {
  position: absolute;
  left: calc(var(--x) * var(--s));
  top:  calc(var(--y) * var(--s));
  font: inherit;
  line-height: 1;      /* the `font` shorthand above resets it */
  color: var(--cream);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: calc(.8 * var(--s));
  text-underline-offset: calc(2.5 * var(--s));
  transition: opacity .3s var(--ease);
}
.send:hover, .send:focus-visible { opacity: .65; }

.formnote {
  position: absolute;
  left: calc(var(--x) * var(--s));
  top:  calc(var(--y) * var(--s));
  margin: 0;
  line-height: 1;
  color: var(--muted);
  font-size: .88em;
  min-height: 1em;
}

/* ==========================================================================
   Narrow / portrait: the canvas approach stops working once the viewport is
   no longer roughly 16:9, so each panel reflows to a centred column. The deck
   still scrolls horizontally — that is the point of the site.
   ========================================================================== */

@media (max-width: 820px), (max-aspect-ratio: 5/4) {
  :root {
    --s: min(100vw / 620, 100dvh / 900);
    --fs: clamp(15px, 4.2vw, 21px);
  }

  body { font-size: var(--fs); }

  .panel {
    display: block;
    padding: calc(var(--fs) * 6.25) 6vw calc(var(--fs) * 5.5);
    overflow-y: auto;
    overscroll-behavior-y: contain;
  }

  .panel > .canvas {
    width: 100%;
    min-height: calc(100dvh - var(--fs) * 11.75);
    height: auto;
    max-width: 34em;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .55em;
    text-align: center;
  }

  /* The chrome anchors to the screen here, not to a 16:9 design box. */
  #stage > .canvas { width: 100%; height: 100%; }

  /* Drop every absolute placement; let the content flow. */
  .ln, .row > a, .row .ico, .field, .send, .formnote {
    position: static;
    left: auto; top: auto; right: auto;
    transform: none;
    white-space: normal;
    line-height: 1.35;
  }
  /* The desktop centring selector is more specific than .ln, so reset it
     explicitly or every centred line is shifted half its width on mobile. */
  .ln[style*="--cx:"] { transform: none; }

  .head { margin-top: 1.1em; }
  .head:first-child { margin-top: 0; }

  #nav {
    position: fixed;
    left: 0; right: 0; top: 0;
    padding: .65em 0 1.2em;
    background: linear-gradient(var(--ink) 68%, transparent);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  #nav::-webkit-scrollbar { display: none; }
  #nav ul {
    width: max-content;
    min-width: 100%;
    display: flex;
    gap: 1.15em;
    justify-content: flex-start;
    flex-wrap: nowrap;
    padding: 0 5vw;
  }
  #nav li { height: auto; }

  .wordmark {
    top: auto;
    bottom: calc(env(safe-area-inset-bottom) + .7em);
    right: 4vw;
    font-size: .9em;
    opacity: .75;
  }

  #fish {
    --fx: 0;
    left: 50%;
    top: auto;
    bottom: calc(env(safe-area-inset-bottom) + 4.2em);
    width: min(42vw, 190px);
    height: auto;
    aspect-ratio: 182.16 / 63.72;
    transform: translateX(-50%) rotate(var(--tilt));
    opacity: .14;
    transition: opacity .35s var(--ease), bottom .45s var(--ease), --tilt .25s linear;
  }
  body[data-panel="0"] #fish {
    bottom: 54%;
    width: min(52vw, 220px);
    opacity: .92;
  }

  /* skills becomes a legible list rather than a scatter */
  .cloud {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .3em .9em;
    margin-top: .6em;
  }

  .contacts { width: 100%; margin: .8em 0; display: grid; gap: .8em; justify-items: center; }
  .row { display: flex; align-items: center; gap: .6em; }
  .row .ico { width: 1.7em; height: 1.7em; flex: none; }

  .cform { width: 100%; display: grid; gap: .5em; justify-items: stretch; margin-top: .6em; }
  .field { width: 100%; height: auto; }
  .field input { height: 2.4em; padding: 0 .6em; }
  .field textarea { height: 7em; padding: .5em .6em; }
  .send { justify-self: center; }
  .formnote { text-align: center; }

  .cert, .certlink, .row > a, .send {
    text-decoration-thickness: 1px;
    text-underline-offset: .18em;
  }

  #contact .canvas { justify-content: flex-start; }
}

/* ── motion preferences ───────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  #fish img { animation: none; }
  #school { display: none; }
  .start { animation: none; opacity: .85; }
  #deck { scroll-behavior: auto; }
  * { transition-duration: .01ms !important; }
}
