/* /projects — the showcase.
   Every class is pjx-* namespaced and scoped under .taxi-w.projects; nothing
   here may style the shared nav/footer. Tokens live on the page root, never on
   :root — the shared sheet owns that.

   Remember 1rem is not 16px: html{font-size} is a fluid function of viewport
   width (css-main.html), so every rem below is a proportional design unit.

   Motion contract, same as the homepage: the hidden start states are gated on
   .pjxjs, which an inline one-liner in content.html sets pre-paint. No class ->
   no JS -> everything renders in its settled end state. */

/* ONE CHAPTER, ONE SET OF NAMES.
   The page is light end to end — the same white the homepage's body and
   /contact use, so arriving here from either does not feel like a different
   site. The hero, the rail cards and the floating plate were all dark objects
   once; they are not any more, and the token block that used to invert them
   for their own scope is gone with them. Every rule below reads a token, never
   a literal colour, so the palette stays a single edit. */

.taxi-w.projects {
  --pjx-bg: #fafcfb;           /* the page — same as /contact */
  --pjx-fg: #040504;           /* = --color--dark-green */
  --pjx-dim: #535450;          /* = --color--dark-green-tint-2 */
  --pjx-dim-2: #737571;        /* the quietest label on light. Was #7c7e79 = 3.98:1
                                  on --pjx-paper #fafcfb; deepened 7% to 4.52:1. Only
                                  consumer is .pjx-row-year, so the token IS the local fix. */
  --pjx-line: rgba(4, 5, 4, .09);
  /* THE CARD, as an object. The shell used to be #ffffff on a #fafcfb page —
     the same colour, effectively, so the card had no body and needed a drop
     shadow to exist at all. Now it is a soft grey-green paper that is plainly
     darker than the page, with a real grey rim. No shadow anywhere: the tone
     step plus the 1px rim is the whole separation. */
  --pjx-card-bg: #e9efeb;
  /* the copy plate under the frost: ink, not paper, so the type reads the
     same whether the shot behind it is a light or a dark web page */
  --pjx-copy-plate: #0a0c0c;
  --pjx-copy-fg: #f2f4ef;
  --pjx-copy-dim: #a7ada6;
  --pjx-rim: #cbd3ce;
  --pjx-rim-hover: #9fa9a4;
  --pjx-surface: #eef4f1;      /* = tint of --color--white */
  --pjx-hover: rgba(4, 5, 4, .045);
  /* ONE SITE GREEN — the same #21f1a8 the nav's "Let's talk" button wears.
     The page used to darken it to #0c7a55 "for contrast on paper", which is
     exactly how the site ended up with two greens. It does not any more: the
     green is the brand mark (filled shapes, the display word in the CTA) and
     anything too small to survive it — micro-labels, focus rings — takes ink,
     not a second green. */
  --pjx-accent: var(--color--lime, #21f1a8);
  --pjx-mint: var(--pjx-accent);
  --pjx-on-mint: #10120f;
  --pjx-mono: ui-monospace, SFMono-Regular, Menlo, 'Courier New', monospace;
  --pjx-ease: cubic-bezier(.215, .61, .355, 1);

  /* what the shared footer band paints itself with — see css-main.html */
  --page-bg: var(--pjx-bg);

  background-color: var(--pjx-bg);
  color: var(--pjx-fg);
  display: block;
}

/* ---------------------------------------------------------------- shared */

.pjx-s {
  position: relative;
  padding-inline: var(--padding--container);
}
.pjx-wrap {
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
}
.pjx-eyebrow {
  font-family: var(--pjx-mono);
  font-size: .625rem;
  line-height: 1;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--pjx-dim);
}
.pjx-rule {
  height: 1px;
  background-color: var(--pjx-line);
  border: 0;
  margin: 0;
}

/* The shared sheet sets `li { list-style-type: disc }` on the ELEMENT, which
   beats `list-style: none` inherited from a parent ul/ol. Every list on this
   page is a layout device, so reset the item itself. */
.taxi-w.projects ol,
.taxi-w.projects ul { list-style: none; margin: 0; padding: 0; }
.taxi-w.projects li { list-style: none; list-style-type: none; }
.taxi-w.projects li::marker { content: none; }

/* Invisible until focused, then a real, visible control. opacity +
   pointer-events rather than a transform alone: a translated-away element
   still peeks whenever an ancestor's clipping box does not start where you
   assumed it did. */
.pjx-skip {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translate(-50%, -100%);
  opacity: 0;
  pointer-events: none;
  z-index: 40;
  padding: .75rem 1.25rem;
  background-color: var(--pjx-mint);
  color: var(--pjx-on-mint);
  font-family: var(--pjx-mono);
  font-size: .75rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: .5rem;
  transition: transform .2s var(--pjx-ease), opacity .2s var(--pjx-ease);
}
.pjx-skip:focus,
.pjx-skip:focus-visible {
  transform: translate(-50%, 1rem);
  opacity: 1;
  pointer-events: auto;
}

/* Entrance reveal — same contract as the homepage's .vx-reveal. */
.pjxjs .pjx-reveal {
  opacity: 0;
  transform: translateY(2.2rem);
}
.pjxjs .pjx-reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity .9s var(--pjx-ease), transform .9s var(--pjx-ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}

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

/* THE MASTHEAD is the Antigravity landing hero, rebuilt in this site's palette:
   a full-viewport light stage, the particle dot-field running behind it, and
   one centred stack — mark, typed headline, lede, two pills. Same geometry,
   same four effects (field / typing caret / staged fade-in / parallax fade on
   scroll); only the colours are ours.

   The field is the SAME engine the homepage and the footer already run
   (dotfield/init.js — itself a port of the Antigravity hero's particle model),
   mounted here as the `hero` kind: host-sized, transparent, light palette. No
   new dependency, no second animation loop.

   Nothing here is dark any more, so there is no chapter to dissolve out of:
   the hero simply IS the page, and the token block below no longer overrides
   anything for it. */
.pjx-hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: clip;
  background-color: var(--pjx-bg);
  padding-block: calc(var(--on-t-nav-spacer, 5.375rem) + 3rem) 6rem;
}
/* The field sits under the copy and outside the text flow. It fades and drifts
   as the hero leaves (init.js writes --pjx-hero-p, 0 -> 1); with no JS it just
   stays at rest, which is a perfectly good hero. */
.pjx-hero-field {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: calc(1 - var(--pjx-hero-p, 0));
  transform: translate3d(0, calc(var(--pjx-hero-p, 0) * -6%), 0);
}
.pjx-hero-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--fluid-container);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* Screen-reader copy for anything the typewriter rewrites character by
   character — the visible span is aria-hidden, this one is the real text. */
.pjx-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.pjx-hero-mark {
  display: inline-flex;
  align-items: center;
  gap: .625rem;
  margin-bottom: 1.75rem;
  color: var(--pjx-fg);
}
.pjx-hero-glyph { width: 1.5rem; height: 1.5rem; flex: none; }
.pjx-hero-mark-t {
  font-family: var(--pjx-mono);
  font-size: .6875rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--pjx-dim);
}

.pjx-hero-title {
  margin: 0;
  max-width: 20ch;
  /* the Antigravity headline's proportions — sentence case, tight, huge —
     set in this site's own display face rather than Google Sans */
  font-size: clamp(2.5rem, 6.4vw, 5.5rem);
  line-height: 1.04;
  letter-spacing: -.02em;
  text-transform: none;
  font-variation-settings: "wght" 620, "wdth" 96;
  color: var(--pjx-fg);
}
.pjx-typed { display: inline; }
/* The caret is the one piece of the hero that must not reflow the headline as
   it blinks — a fixed-width inline-block, never a character. */
.pjx-caret {
  display: inline-block;
  width: .06em;
  height: .78em;
  margin-left: .06em;
  vertical-align: -.06em;
  background-color: var(--pjx-fg);
  opacity: 0;
}
/* No JS -> no typing -> no caret: the headline renders whole and still. */
.pjxjs .pjx-caret { opacity: 1; animation: pjx-blink .55s steps(1, end) infinite; }
.pjxjs .pjx-hero-title.is-typed .pjx-caret { animation-delay: .2s; }
@keyframes pjx-blink { 50% { opacity: 0; } }

.pjx-hero-lede {
  margin: 1.75rem 0 0;
  max-width: 42rem;
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--pjx-dim);
}
.pjx-hero-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: .75rem;
  margin-top: 2.5rem;
}
/* The two Antigravity pills: a solid one and a frosted one. */
.pjx-btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .95rem 1.5rem;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: .9375rem;
  line-height: 1;
  font-variation-settings: "wght" 550;
  text-decoration: none;
  transition: background-color .35s var(--pjx-ease), border-color .35s var(--pjx-ease), transform .35s var(--pjx-ease);
}
.pjx-btn-ico { width: 1rem; height: 1rem; flex: none; }
.pjx-btn.is-primary { background-color: var(--pjx-mint); color: var(--pjx-on-mint); }
.pjx-btn.is-primary:hover, .pjx-btn.is-primary:focus-visible { background-color: #19d894; }
.pjx-btn.is-secondary {
  background-color: color-mix(in srgb, var(--pjx-surface) 70%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border-color: var(--pjx-line);
  color: var(--pjx-fg);
}
.pjx-btn.is-secondary:hover, .pjx-btn.is-secondary:focus-visible { background-color: var(--pjx-surface); }
.pjx-btn:hover { transform: translateY(-1px); }
.pjx-btn:focus-visible { outline: 2px solid var(--pjx-fg); outline-offset: 3px; }

/* The staged fade-in under the headline — same contract as .pjx-reveal, but
   played once on load rather than on scroll, because it is above the fold. */
.pjxjs [data-pjx-fade], .pjxjs [data-pjx-mark] { opacity: 0; transform: translateY(1rem); }
.pjxjs [data-pjx-fade].is-in, .pjxjs [data-pjx-mark].is-in {
  opacity: 1;
  transform: none;
  transition: opacity .8s var(--pjx-ease), transform .8s var(--pjx-ease);
  transition-delay: calc(var(--i, 0) * 90ms);
}

/* --- the numbers, lifted out of the hero into their own quiet band ------- */
.pjx-facts-s { padding-block: 0 5rem; }
.taxi-w.projects .pjx-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1.75rem 3rem;
  padding-top: 2.5rem;
  border-top: 1px solid var(--pjx-line);
}
.pjx-fact { display: flex; flex-direction: column; gap: .4rem; }
.pjx-fact b {
  font-size: 2.25rem;
  line-height: 1;
  font-variation-settings: "wght" 750, "wdth" 93;
  color: var(--pjx-fg);
}

/* ---------------------------------------------------------------- the rail */

/* Sticky-pinned horizontal scroll. The tall wrapper is the scroll budget; the
   stage pins inside it; init.js turns wrapper progress into --pjx-x on the
   track — one property write per frame moves everything. The cards are the
   homepage journey (jr-) cards resized for web-page shots: the shot fills the
   card and NEVER transforms, so nothing can drift out of frame while the rail
   scrubs. */
/* The BASE state is the no-JS state: a native, swipeable, scroll-snapped list.
   Nothing is hidden and the page is not eight empty screens tall. The pinned
   rail is layered on top of it under .pjxjs, and reduced motion peels that
   layer back off — so the fallback is written once and reached two ways. */
.pjx-rail-s {
  padding-inline: 0;
  background-color: var(--pjx-bg);
}
.pjx-rail-wrap { position: relative; }
.pjx-rail-stage {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding-bottom: 1rem;
}
.pjx-rail-bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap);
  padding-inline: var(--padding--container);
}
.pjx-rail-count { font-family: var(--pjx-mono); font-size: .625rem; color: var(--pjx-dim); }
.pjx-rail-count b { color: var(--pjx-fg); font-variation-settings: "wght" 500; }
.pjx-rail-viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 1rem;
}
.pjx-track {
  /* Card geometry, one lever each — the same levers the homepage journey
     section exposes (--jr-card-h / --jr-pad), sized for portrait shots.
     The card is a tall 8:15 portrait: half again as tall per unit of width as
     the 4:5 it started at, because these are WEB PAGES — the taller the frame,
     the more of the real page each card shows. The height is pushed to the
     ceiling the pinned stage allows (stage = 100svh minus the nav spacer and
     the bottom padding), so nothing overflows. */
  --pjx-card-h: min(84vh, 53rem);
  --pjx-card-w: calc(var(--pjx-card-h) * .5333); /* 8:15 — the plate crops' ratio */
  --pjx-pad: 2.25rem;                            /* half the visual gutter */
  --pjx-shell: var(--pjx-card-bg);
  display: flex;
  align-items: center;
  padding-inline: var(--padding--container);
  width: max-content;
}
.pjx-slide {
  flex: none;
  width: calc(var(--pjx-card-w) + var(--pjx-pad) * 2);
  padding-inline: var(--pjx-pad);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: center;
}

/* --- the pinned rail, JS only ------------------------------------------- */
.pjxjs .pjx-rail-wrap {
  /* 10 slides; the multiplier is the scroll budget per slide. */
  height: calc(100vh + 10 * 74vh);
}
.pjxjs .pjx-rail-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  justify-content: center;
  overflow: hidden;
  padding-top: var(--on-t-nav-spacer, 5.375rem);
  padding-bottom: 2rem;
}
.pjxjs .pjx-rail-viewport {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: visible;
  scroll-snap-type: none;
  padding-bottom: 0;
}
.pjxjs .pjx-track {
  position: absolute;
  inset: 0;
  width: auto;
  padding-inline: 8vw;
  will-change: transform;
  transform: translate3d(calc(var(--pjx-x, 0) * 1px), 0, 0);
}
.pjxjs .pjx-slide { scroll-snap-align: none; }

/* No timeline here. The homepage journey threads its cards on a line because
   they are four chapters of one story; these ten are ten separate pieces of
   work, and the line plus the alternating up/down offset only broke the row
   into ragged halves. One straight row, every card the same height, the eye
   free to compare them — the rail says everything the line was saying. */

/* --- the card: the homepage journey card, resized for web-page shots and
   turned over to the light theme. Paper shell, 1px ink rim, a soft lift
   shadow in place of the old dark-on-dark separation, 5px bezel, the shot
   filling the whole card, a progressive backdrop-blur glass carrying the
   copy, a frost (not an ink scrim) for contrast, and a sheen that sweeps on
   hover. The image itself never transforms. ------------------------------- */
.pjx-card {
  position: relative;
  z-index: 1;
  display: block;
  flex: none;
  width: var(--pjx-card-w);
  height: var(--pjx-card-h);
  color: var(--pjx-fg);
  text-decoration: none;
  background-color: var(--pjx-shell);
  border: 1px solid var(--pjx-rim);
  border-radius: 1.5rem;
  padding: .3125rem;
  overflow: clip;
  /* Flat by design — no lift shadow anywhere. The 1px grey rim is the whole
     separation, so nothing here may reintroduce a box-shadow. */
  transition: border-color .5s cubic-bezier(.25, .46, .45, .94);
}
.pjx-frame {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 1.1875rem; /* outer radius minus the .3125rem bezel */
  overflow: clip;
  transition: filter .55s cubic-bezier(.25, .46, .45, .94);
}
.pjx-shot { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 0; }

.pjx-glass, .pjx-scrim, .pjx-sheen { pointer-events: none; position: absolute; }
.pjx-glass {
  z-index: 1;
  inset: auto .3125rem .3125rem;
  /* only as tall as the copy needs — the shot is the point of the card, so
     the plate stops just above the title instead of eating two thirds of it */
  height: 46%;
  border-radius: 0 0 1.1875rem 1.1875rem;
  overflow: clip;
  transition: height .55s cubic-bezier(.25, .46, .45, .94);
}
.pjx-glass span { position: absolute; inset: 0; }
.pjx-glass span:nth-child(1) { -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px); -webkit-mask-image: linear-gradient(to top, #000 0 30%, #0000 78%); mask-image: linear-gradient(to top, #000 0 30%, #0000 78%); }
.pjx-glass span:nth-child(2) { -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); -webkit-mask-image: linear-gradient(to top, #000 0 18%, #0000 58%); mask-image: linear-gradient(to top, #000 0 18%, #0000 58%); }
.pjx-glass span:nth-child(3) { -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px); -webkit-mask-image: linear-gradient(to top, #000 0 8%, #0000 38%); mask-image: linear-gradient(to top, #000 0 8%, #0000 38%); }
.pjx-glass span:nth-child(4) { -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); -webkit-mask-image: linear-gradient(to top, #000 0 0%, #0000 20%); mask-image: linear-gradient(to top, #000 0 0%, #0000 20%); }
.pjx-scrim {
  z-index: 2;
  inset: auto .3125rem .3125rem;
  height: 46%;
  border-radius: 0 0 1.1875rem 1.1875rem;
  /* The frost has to finish fully opaque: half the shots here are DARK web
     pages, and ink copy has to survive landing on one. */
  /* the frost lands on the CARD's tone, not the page's, so the copy plate and
     the bezel around it are the same paper */
  background-image: linear-gradient(rgba(8, 10, 10, 0) 0%, rgba(8, 10, 10, .06) 12%, rgba(8, 10, 10, .22) 22%, rgba(8, 10, 10, .48) 31%, rgba(8, 10, 10, .74) 39%, rgba(8, 10, 10, .9) 47%, rgba(8, 10, 10, .95) 56%, rgba(8, 10, 10, .96) 100%);
}
/* No backdrop-filter (old Safari, forced-fallback GPUs): the frost alone must
   carry the copy's contrast, so it starts earlier and lands sooner. */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .pjx-scrim { background-image: linear-gradient(rgba(8, 10, 10, 0) 0%, rgba(8, 10, 10, .1) 10%, rgba(8, 10, 10, .34) 20%, rgba(8, 10, 10, .66) 29%, rgba(8, 10, 10, .88) 37%, rgba(8, 10, 10, .97) 46%, var(--pjx-copy-plate) 56%); }
}
.pjx-sheen {
  z-index: 3;
  inset: .3125rem;
  border-radius: 1.1875rem;
  background-image: linear-gradient(105deg, #0000 42%, #ffffff5c 50%, #0000 58%);
  transform: translateX(-120%);
  opacity: 0;
  transition: transform .85s cubic-bezier(.25, .46, .45, .94), opacity .4s linear;
}

.pjx-info {
  position: absolute;
  z-index: 4;
  inset: auto 0 0;
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: 0 1.15rem 1.15rem;
}
.pjx-info-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; }
.pjx-title {
  font-family: Quicksand, Arial, sans-serif;
  font-size: 1.5rem;
  line-height: 1.05;
  letter-spacing: .01rem;
  color: var(--pjx-copy-fg);
}
.pjx-badge {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  background-color: var(--pjx-mint);
  color: #10120f;
  font-family: var(--pjx-mono);
  font-size: .625rem;
}
.pjx-kicker {
  font-family: var(--pjx-mono);
  font-size: .5625rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--pjx-copy-dim);
}
.pjx-body {
  margin: 0;
  font-family: var(--pjx-mono);
  font-size: .75rem;
  font-weight: 300;
  line-height: 1.5;
  color: var(--pjx-copy-fg);
  opacity: .78;
}
.pjx-foot { display: flex; align-items: center; gap: .55rem; margin-top: .25rem; }
.pjx-chip {
  display: inline-flex;
  align-items: center;
  gap: .28rem;
  font-family: var(--pjx-mono);
  font-size: .5625rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--pjx-copy-fg);
  opacity: .8;
}
.pjx-ico { width: .75rem; height: .75rem; flex: none; }
.pjx-pill {
  display: inline-flex;
  align-items: center;
  gap: .18rem;
  margin-left: auto;
  padding: .3rem .6rem;
  border-radius: 999px;
  /* inverted against the frost it now sits on */
  background-color: var(--pjx-copy-fg);
  color: var(--pjx-copy-plate);
  font-family: var(--pjx-mono);
  font-size: .5625rem;
  letter-spacing: .04em;
  transition: background-color .45s cubic-bezier(.25, .46, .45, .94), color .45s cubic-bezier(.25, .46, .45, .94);
}
.pjx-pill-sep { opacity: .45; }

/* hover deepens the same grey — the card never lifts off the page */
.pjx-card:hover, .pjx-card:focus-visible { border-color: var(--pjx-rim-hover); }
.pjx-card:hover .pjx-frame, .pjx-card:focus-visible .pjx-frame { filter: saturate(1.12) brightness(1.05); }
.pjx-card:hover .pjx-glass, .pjx-card:focus-visible .pjx-glass { height: 50%; }
.pjx-card:hover .pjx-sheen, .pjx-card:focus-visible .pjx-sheen { opacity: 1; transform: translateX(120%); }
.pjx-card:hover .pjx-pill, .pjx-card:focus-visible .pjx-pill { background-color: var(--pjx-mint); color: var(--pjx-on-mint); }
.pjx-card:focus-visible { outline: 2px solid var(--pjx-fg); outline-offset: 3px; }

/* ---------------------------------------------------------------- dossier */

.pjx-index-s { padding-block: 8rem; }
.pjx-index-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--gap); margin-bottom: 2.5rem; }
.pjx-index {
  position: relative;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--pjx-line);
}
.pjx-row { border-bottom: 1px solid var(--pjx-line); }
.pjx-row-link {
  display: grid;
  grid-template-columns: 3.5rem 1fr auto;
  align-items: center;
  gap: var(--gap);
  padding: 1.375rem .5rem;
  color: inherit;
  text-decoration: none;
  transition: background-color .35s var(--pjx-ease), padding-left .35s var(--pjx-ease);
}
.pjx-row-link:hover, .pjx-row-link:focus-visible {
  background-color: var(--pjx-hover);
  padding-left: 1.25rem;
}
.pjx-row-link:focus-visible { outline: 2px solid var(--pjx-fg); outline-offset: -2px; }
.pjx-row-n { font-family: var(--pjx-mono); font-size: .625rem; color: var(--pjx-dim); }
.pjx-row-main { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; min-width: 0; }
.pjx-row-name { font-size: 1.5rem; line-height: 1.1; font-variation-settings: "wght" 650, "wdth" 95; }
.pjx-row-cat { font-family: var(--pjx-mono); font-size: .625rem; text-transform: uppercase; letter-spacing: .06em; color: var(--pjx-dim); }
.pjx-row-year { font-family: var(--pjx-mono); font-size: .625rem; color: var(--pjx-dim-2); }
/* The inline thumbnail is the touch/no-pointer/reduced-motion substitute for
   the floating plate — same information, no cursor required. */
.pjx-row-thumb {
  display: none;
  width: 4rem;
  height: 2.75rem;
  object-fit: cover;
  object-position: 50% 0;
  border-radius: .25rem;
  border: 1px solid var(--pjx-line);
}

/* The floating plate. Fixed so it is never clipped by the list's overflow, and
   pointer-events:none so it can never steal the pointermove it is driven by. */
.pjx-plate {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 3;
  width: 14rem;
  aspect-ratio: 8 / 15;
  border-radius: 1rem;
  overflow: clip;
  border: 1px solid var(--pjx-rim, rgba(4, 5, 4, .10));
  background-color: var(--pjx-card-bg, #e9efeb);
  /* flat, like every other card on the page — the rim carries the edge */
  padding: .1875rem;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  /* no opacity transition here: the rAF loop lerps opacity itself, and a CSS
     transition restarted by every per-frame write only smears the fade */
  display: none;
}
.pjx-plate img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 0; border-radius: .8125rem; }
.pjxjs .pjx-plate { display: block; }

/* ---------------------------------------------------------------- wall + cta */

.pjx-wall-s { padding-block: 6rem 8rem; }
/* The section title is centred; the grid below gets real breathing room so the
   headline reads as a header, not a caption stuck to the first row of cards. */
.pjx-wall-title { text-align: center; }
/* `.taxi-w.projects ul` (the list reset at the top of this file) is TWO classes
   and beats a bare `.pjx-wall` — the margin below silently did nothing until
   this selector was raised to match it. Any future spacing on a list here has
   to carry the page class too. */
.taxi-w.projects .pjx-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1.5rem;
  margin: 9rem 0 0;
  padding: 0;
  list-style: none;
}
.pjx-wall-card {
  /* same body and same grey rim as the rail cards — a card, not a hole */
  background-color: var(--pjx-card-bg);
  border: 1px solid var(--pjx-rim);
  border-radius: var(--radius--small);
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: .875rem;
}
.pjx-wall-name { font-size: 1rem; font-variation-settings: "wght" 650; letter-spacing: -.01em; }
.pjx-wall-note { font-size: .8125rem; line-height: 1.45; color: var(--pjx-dim); margin: 0; }

/* "your own?" used to wear the site's one green, #21f1a8, straight.
   On this page it lands on --pjx-paper #fafcfb and measured 1.43:1 where large
   text needs 3:1 -- at BOTH widths (62.5px desktop, 26px mobile), not just the
   phone. Mint is very light, so nothing subtle clears 3:1 on near-white: the
   closest passing colour in the whole sRGB cube is dE2000 ~19 away. #17a573 is
   the hue- and saturation-locked variant -- the same mint, darkened only --
   at 3.06:1.

   This is a LOCAL override for this one heading on this one page. It is
   deliberately written as a literal rather than a token so it cannot travel:
   --pjx-accent, --pjx-mint and --color--lime all still resolve to #21f1a8, and
   the signature mint is unchanged everywhere it already has enough contrast
   (the nav button, the case-study accents, the focus ring). */
.taxi-w.projects .pjx-cta-title .c-lime-off { color: #17a573; }

.pjx-cta-s { padding-block: 6rem 9rem; border-top: 1px solid var(--pjx-line); }
.pjx-cta-inner { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2rem; }
.pjx-cta-title { margin: 0; max-width: 20ch; }
.pjx-cta-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 1.5rem; }

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

@media screen and (max-width: 991px) {
  .pjxjs .pjx-rail-wrap { height: calc(100vh + 10 * 62vh); }
  /* Same tokens, phone numbers: the card is sized from the viewport width and
     keeps the 8:15 ratio, capped so the tall card still clears the stage. */
  .pjx-track {
    /* the 74vh term is the stage cap: whichever limit bites, the RATIO holds,
       so object-fit never has a side to crop */
    --pjx-card-w: min(78vw, 26rem, calc(70vh / 1.875));
    --pjx-card-h: calc(var(--pjx-card-w) * 1.875);
    --pjx-pad: .75rem;
  }
  .pjxjs .pjx-track { padding-inline: 6vw; }
  /* The 2px blur layer is invisible over a screenshot at phone size — trim the
     compositor work. */
  .pjx-glass span:nth-child(1) { display: none; }
  /* The shorter card puts the copy higher up the shot, where the desktop
     gradient is still thin — start the frost earlier. */
  .pjx-scrim { background-image: linear-gradient(rgba(250, 252, 251, 0) 22%, rgba(250, 252, 251, .88) 50%, #fafcfb 72%); }
  .pjx-index-s, .pjx-wall-s { padding-block: 5rem; }
  .pjx-row-link { grid-template-columns: 2.5rem 1fr; }
  .pjx-row-year { display: none; }
}

@media screen and (max-width: 767px) {
  .pjx-hero { padding-block: calc(var(--on-t-nav-spacer, 5.375rem) + 2rem) 4rem; }
  /* 1rem is fluid, so the desktop clamp's 2.5rem floor lands near 26px at
     phone width — too quiet for a masthead. Let it wrap instead. */
  .pjx-hero-title { font-size: clamp(3.4rem, 10vw, 4.4rem); max-width: 14ch; }
  .pjx-hero-mark { margin-bottom: 1.25rem; }
  .pjx-hero-lede { font-size: .9375rem; margin-top: 1.25rem; }
  .pjx-hero-cta { margin-top: 1.75rem; }
  .pjx-btn { padding: .85rem 1.25rem; font-size: .875rem; }
  .pjx-facts-s { padding-block: 0 3.5rem; }
  .pjx-fact b { font-size: 1.75rem; }
  .pjx-row-name { font-size: 1.25rem; }
  .pjx-info { padding: 0 1rem 1rem; }
  /* The mobile nav pills sit lower than the desktop bar — same offset the
     homepage's ring stage uses. */
  .pjxjs .pjx-rail-stage { padding-top: 7rem; }
}

/* Coarse pointer: the plate has nothing to follow, so every row carries its own
   thumbnail instead. */
@media (hover: none), (pointer: coarse) {
  .pjxjs .pjx-plate { display: none; }
  .pjx-row-thumb { display: block; }
  .pjx-row-link { grid-template-columns: 2.5rem 1fr auto; }
}

/* ---------------------------------------------------------------- reduced motion

   Peel the .pjxjs layer back off and the base (no-JS) state shows through: a
   native swipeable list, no pin, no scrub, no floating plate. init.js also
   never starts its loop, and its change listener clears --pjx-x. */

@media (prefers-reduced-motion: reduce) {
  .pjxjs .pjx-reveal { transform: none; }
  .pjxjs .pjx-reveal.is-in { transition: opacity .5s linear; transition-delay: 0s; }
  .pjxjs .pjx-plate { display: none; }
  .pjx-row-thumb { display: block; }
  .pjx-card, .pjx-frame, .pjx-glass, .pjx-sheen, .pjx-pill, .pjx-row-link, .pjx-btn { transition: none; }
  /* the caret stops blinking, and init.js prints the headline in one go */
  .pjxjs .pjx-caret { animation: none; opacity: 0; }
  .pjxjs [data-pjx-fade], .pjxjs [data-pjx-mark] { transform: none; }
  .pjxjs [data-pjx-fade].is-in, .pjxjs [data-pjx-mark].is-in { transition: opacity .4s linear; transition-delay: 0s; }
  /* the field renders a still frame; the scroll-linked fade goes with it */
  .pjx-hero-field { opacity: 1; transform: none; }

  .pjxjs .pjx-rail-wrap { height: auto; }
  .pjxjs .pjx-rail-stage { position: static; height: auto; overflow: visible; padding-bottom: 1rem; }
  .pjxjs .pjx-rail-viewport {
    position: static;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    padding-bottom: 1rem;
  }
  .pjxjs .pjx-track { position: static; width: max-content; transform: none; padding-inline: var(--padding--container); }
  .pjxjs .pjx-slide { scroll-snap-align: center; }
}

/* ═══ RTL — html[dir="rtl"] only ═══════════════════════════════════════════
   Provably inert for EN/DE: the attribute is written by the build and only
   the /ar/ tree carries it.

   The counters are the one thing RTL must NOT mirror. `01 / 10` is a single
   token — position within a set — but it is authored as three inline boxes
   (`01`, the separator, `10`), and inline boxes lay out in the CONTAINER's
   direction. Measured on /ar/projects before this rule: `.pjx-pill` rendered
   `10 / 01` and `.pjx-rail-count` rendered `/ 10  01` — i.e. "ten of one".
   The bidi algorithm cannot fix it, because each number is its own run.
   Give the counter an LTR inline context; its BOX still mirrors with the
   card, which is correct. Everything else on this page mirrors as-is —
   the sheet is logical-property native. */
html[dir="rtl"] :is(.pjx-pill, .pjx-rail-count) { direction: ltr; }

/* The spec chips are single text nodes that mix digits with Latin words —
   `6 KB JS` rendered `KB JS 6`. plaintext derives their direction from their
   own content, so a chip that ever carries Arabic still reads RTL. */
html[dir="rtl"] .pjx-chip { unicode-bidi: plaintext; }

/* The stack lists hold technical terms that stay Latin in every locale
   (`clamp()` rendered `)(clamp` — the parens are neutrals and flipped). */
html[dir="rtl"] .pjx-stack li { unicode-bidi: plaintext; }
