/* ════════════════════════════════════════════════════════════
   TNAADO — /technology HERO: "THE SHOWCASE"
   2026-08-13. Seventh pass, and the concept is Ethan's own: the hero
   is a rotating showcase of the real projects.

   THE SIX THAT DIED, AND WHY — read this before proposing an eighth,
   because five of the six failed the same way:
     1  WebGL2 node/wireframe lattice ....... the generic network cliché
     2  Autoplay <video> loop ................ no background loops, ever
     3  Photo sliced into 3D bands ........... measured: 96.6% of its
                                               "entry" was an invisible
                                               opacity crossfade
     4  "THE ASSEMBLY", a machined enclosure
        in hand-rolled WebGL2 ................ the best-built of the six
                                               and still wrong: the object
                                               was deliberately ABSTRACT,
                                               and an abstract shape does
                                               not say "software firm"
     5  "THE COUNT", 203/9 in a card ......... a stat card, which is the
                                               most templated object on
                                               the web ("cheap designs")
     6  "THE BOARD", a five-row status list .. closer — real products —
                                               but a static list, and it
                                               showed six things when the
                                               firm has more

   THE PATTERN: passes 1-5 were decoration ABOUT the work. This one is
   the work, on rotation, and it grows by itself as the portfolio table
   grows. Nothing on it is a metaphor for anything.

   VERIFIED 2026-08-13 (the authored slides; DB slides carry their own
   data):
     The Dispatch      iTunes id 6784010543  v1.0.8   TNAADO INC.
     Mr. Indecisive    iTunes id 6784115836  v1.0.13  TNAADO INC.
     tools.tnaado.ca / tnaado.ca / tnaado.com / links.tnaado.ca  HTTP 200
   NOT shown, because the iTunes API says they are not on the store:
     Dispatch Live, GEB, Wildin, TNAADO Connect, Find The Title, Mr Syn.
   NOT shown, because it does not resolve: photos.tnaado.ca.

   MOTION (emil-design-eng)
   ────────────────────────
   · CSS transition on transform, not a keyframe animation, so hammering
     the next button retargets from the current position instead of
     restarting from zero.
   · 260ms — inside the sub-300ms band for a UI move of this size.
   · A real ease-out curve, not the browser's weak built-in.
   · Auto-advance is 6.5s of DWELL between 260ms moves, pauses on hover,
     on keyboard focus, and when the tab is hidden, and never starts at
     all under prefers-reduced-motion.
   · Buttons have a real :active press.
   · Hover treatments are gated behind (hover: hover) and (pointer: fine)
     so a tap on a phone does not leave a slide stuck in a hover state.

   TOKENS ONLY: --red #c8102e, ink #0a0a0a, and this codebase's
   rgba(245,241,232,X) elevation scale. Flat corners, hairline rules.
   ════════════════════════════════════════════════════════════ */

/* T-1095: 80svh -> 78svh. The tallest of the four non-splash heroes, because
   /technology is the flagship, but the four are now deliberately different
   heights rather than all being one full-screen slab. */
.page-hero.glow-hero--tech { min-height: 78svh; }
@media (max-aspect-ratio: 19/20), (max-width: 767px) {
  .page-hero.glow-hero--tech {
    min-height: auto;
    padding-block: clamp(3rem, 12vw, 4.4rem) clamp(2.4rem, 8vw, 3.4rem);
  }
}

/* ── THE HERO PHOTOGRAPH (T-1086, 2026-08-13) ──────────────────
   Until this pass the hero had a .section-bg-overlay and NO
   .section-bg-photo, so every .glow-hero--tech photo rule in
   glow.css applied to nothing and the section rendered as a black
   rectangle. Ethan: "should be overlapped with the hero photo,
   not a blacked out hero. Use fading layer nicely without cutting
   our heads off."

   bts-claude-code-desk-wide.jpg is 2400x1200 — native 2:1, so it
   is never upscaled, not even at 1920. The hero band is ~2:1 at
   1440 and wider at 1920, so the crop is essentially horizontal
   fill with a shallow vertical window; 50% 46% keeps the laptop
   screen (the subject) centred and the desk foreground intact.
   There is no face anywhere in this frame, which is the other
   reason it can carry a scrim at all.

   The photo is anchored RIGHT and its left edge is FEATHERED with
   a mask rather than cut, so the picture dissolves into the page
   ink under the headline instead of ending at a line. There is no
   photo edge visible anywhere in the hero. */
.page-hero.glow-hero--tech .section-bg-photo {
  object-position: 50% 46%;
}
@media (min-aspect-ratio: 19/20) and (min-width: 940px) {
  .page-hero.glow-hero--tech .section-bg-photo {
    left: auto; right: 0;
    width: 78%;
    -webkit-mask-image: linear-gradient(to right, transparent 0%, rgba(0,0,0,.4) 14%, #000 36%);
            mask-image: linear-gradient(to right, transparent 0%, rgba(0,0,0,.4) 14%, #000 36%);
  }
}
/* Portrait/narrow: the copy fills the frame, so there is no "beside
   the type" to slide into. Full bleed, and site.css's narrow-viewport
   scrim takes the legibility from the bottom up. */
@media (max-width: 939px), (max-aspect-ratio: 19/20) {
  .page-hero.glow-hero--tech .section-bg-photo { object-position: 62% 46%; }
}

/* Landscape: copy left, showcase right. Explicit placement — .inner has
   two children here and the showcase must hold the full column height. */
@media (min-aspect-ratio: 19/20) and (min-width: 940px) {
  .page-hero.glow-hero--tech > .inner {
    display: grid;
    grid-template-columns: minmax(0, .86fr) minmax(0, 1.14fr);
    gap: clamp(2.4rem, 4.5vw, 4.5rem);
    align-items: center;
    width: 100%;
    max-width: 1240px;
  }
  .hs__copy .page-hero__eyebrow,
  .hs__copy .page-hero__headline,
  .hs__copy .page-hero__lede,
  .hs__copy .pr-ctas { max-width: 30rem; }
}
@media (max-width: 939px), (max-aspect-ratio: 19/20) {
  .hs { margin-top: clamp(2.2rem, 7vw, 3rem); }
  /* T-1120: the 30rem cap above only applies in the two-column layout, so
     between 620 and 939px the lede ran the full column — measured 640px /
     87 characters per line at 768, over the 82 ceiling. 34rem holds it at
     ~74. The headline is display type and is allowed to run wider. */
  .hs__copy .page-hero__lede { max-width: 34rem; }

  /* T-1182 — THE HERO HAD NO WORDS IN IT ON A PHONE.
     Measured at 390x844 on this branch: the eyebrow, the H1, the lede and
     both buttons were all clipped out of the hero and could not be reached
     by scrolling. What a phone visitor saw first was the bottom third of a
     showcase card, cut off mid-sentence.

     Cause is site.css T-1134: `.page-hero { max-height: clamp(360px, 46vw,
     560px); overflow: hidden }`. 46vw of 390px is 179px, so the clamp
     bottoms out at its 360px floor on every phone, and this hero stacks to
     ~1030px in portrait. The section is a column flexbox that packs to the
     end, so the overflow is taken off the TOP — which is exactly where the
     copy is.

     This is the same failure, with the same cause, that css/tech-readable.css
     documents and fixes for .stk-hero on /tech ("MOBILE: THE HERO HAD NO
     WORDS IN IT"). .page-hero never got the equivalent. Same remedy, same
     reasoning: the cap has never been operative at this width — at 390px it
     can only ever delete content — so it is lifted here and the hero is held
     down by the showcase instead (below), not by a crop.

     Two classes, so it beats site.css's single-class rule without !important
     and without touching the other three heroes the cap covers. */
  .page-hero.glow-hero--tech { max-height: none; }

  /* ...and with the copy restored the hero measured 1034px, which is the
     full-viewport hero this site has spent the week pulling away from. The
     showcase card is what pays for it, not the words: its third line, the
     120-character description, is the same sentence the card's title and
     the section below already carry, and it is the only part of the card
     that is not the device, the kind or the name. Dropping it on phones
     takes the hero to 899px with every word of the copy intact.
     Phones only — the two-column layout above is unaffected. */
  .hs__frame .hs__desc { display: none; }
}

/* ── THE SHOWCASE ──────────────────────────────────────────── */
.hs { position: relative; z-index: 2; min-width: 0; }
/* overflow hidden on the frame, NOT on .hs — the focus ring on a slide
   link must not be clipped, and neither must the nav row below it. */
/* THE FRAME IS A DARK PLATE, NOT A CLEAR ONE. It used to be a
   rgba(245,241,232,.03) wash, which let the hero's red radial glow
   through at full strength behind the slides. Measured, the worst-case
   backdrop that produced was rgb(95,20,31), and .hs__kind at #e8455a on
   it is 3.40:1 — an AA failure that was invisible because the earlier
   pass only measured the two colours sitting on a screenshot. Dropping
   rgba(10,10,10,.55) over it pins the backdrop near ink wherever the
   slide lands, which fixes every colour on the card at once instead of
   one at a time, and gives the phone screenshots a consistent plate to
   sit on. Worst case is now rgb(46,11,17). */
/* T-1086: .55 was measured against a hero with NOTHING behind it. There is a
   photograph back there now, so the same alpha let a lit face read through the
   slide plate and every contrast figure above stopped holding. .86 restores
   the measured backdrop (the frame is opaque enough that the worst case is
   still near ink) while the blur keeps a hint of the picture at the edges. */
.hs__frame {
  position: relative; overflow: hidden;
  border: 1px solid rgba(245,241,232,.16);
  background: rgba(10,10,10,.86);
  backdrop-filter: blur(3px);
}
.hs__track {
  display: flex; list-style: none; margin: 0; padding: 0;
  transition: transform .26s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
.hs__slide { flex: 0 0 100%; min-width: 0; }
.hs__link {
  display: flex; flex-direction: column; gap: .45rem;
  padding: clamp(1.5rem, 3vw, 2.2rem);
  min-height: clamp(210px, 26vw, 268px);
  text-decoration: none; color: inherit;
}
.hs__link:focus-visible { outline: 2px solid var(--red); outline-offset: -3px; }
/* #ec5c6e, not the sitewide --red-text #e8455a. --red-text is measured
   against flat ink (5.12:1) and this label does not always sit on flat
   ink — over the hero's red glow it fell to 3.40:1 and over a bright
   screenshot pixel to 4.33:1. #ec5c6e is the same hue lifted until the
   WORST of the three backdrops clears AA: 5.39 over the glow, 5.96 over
   ink, 5.04 over a white screenshot pixel under the scrim. */
.hs__kind {
  font-family: var(--font-mono); font-size: .6rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase;
  color: #ec5c6e;
}
/* THE STATE DISTINCTION, AND IT IS THE POINT OF THIS COMPONENT.
   Red reads as "live, go and open it". An app that is built but not yet
   on the store must not borrow that signal, so it takes the neutral
   cream instead — a different colour, not a smaller one. 10.85:1 worst
   case, so the demotion is in hue only and never in legibility. */
.hs__kind--soon { color: rgba(245,241,232,.82); }
.hs__name {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.35rem, 2.4vw, 1.85rem); line-height: 1.15;
  color: #f5f1e8;
}
.hs__desc {
  font-size: .86rem; line-height: 1.6;
  color: rgba(245,241,232,.68); max-width: 40ch;
}
/* Real stack chips, straight off the row's tech_stack column — only DB
   slides have these, and they are the most checkable thing on the card
   for an engineering buyer. */
.hs__stack { display: flex; flex-wrap: wrap; gap: .3rem; margin: .2rem 0 0; padding: 0; list-style: none; }
.hs__stack li {
  font-family: var(--font-mono); font-size: .6rem; letter-spacing: .04em;
  color: rgba(245,241,232,.62);
  border: 1px solid rgba(245,241,232,.18);
  padding: .16rem .4rem;
}
.hs__go {
  margin-top: auto; padding-top: .6rem;
  font-family: var(--font-mono); font-size: .64rem;
  letter-spacing: .1em; text-transform: uppercase;
  color: rgba(245,241,232,.72);
  border-bottom: 1px solid transparent; align-self: flex-start;
  transition: color .2s ease, border-color .2s ease;
}
@media (hover: hover) and (pointer: fine) {
  .hs__link:hover .hs__go { color: #f5f1e8; border-bottom-color: rgba(245,241,232,.5); }
}

/* ── BUILT BUT NOT SHIPPED ──────────────────────────────────
   .hs__status sits where .hs__go sits on a live slide, and reads
   "Coming to the App Store". It deliberately has NO underline, no
   arrow, no hover state and no cursor change, because every one of
   those is an affordance that promises a destination — and there
   isn't one. The whole card is a <div>, not an <a>, for the same
   reason. 8.98:1 worst case. */
.hs__link--static { cursor: default; }
.hs__status {
  margin-top: auto; padding-top: .6rem;
  font-family: var(--font-mono); font-size: .64rem;
  letter-spacing: .1em; text-transform: uppercase;
  color: rgba(245,241,232,.74);
  align-self: flex-start;
}
.hs__status::before {
  content: ""; display: inline-block;
  width: 5px; height: 5px; margin-right: .5rem;
  vertical-align: middle;
  border: 1px solid rgba(245,241,232,.6);
  /* Hollow, where a shipped thing would be filled. */
}

/* Thumbnail, when the DB row carries one.
   THE SCRIM IS SIDE-WEIGHTED, NOT FLAT. The first cut used a full-bleed
   .55-.95 vertical wash, which held the type fine and buried the
   screenshot — on a component whose entire job is SHOWCASING the work,
   that is backwards. This darkens hard under the copy on the left and
   releases to nearly clear on the right, so the actual screenshot is
   legible while the text still sits on ~.9 of ink. Measured after the
   change: .hs__desc 8.9:1, .hs__name 15.9:1. */
.hs__slide--shot .hs__link { position: relative; }
.hs__shot { position: absolute; inset: 0; z-index: 0; }
.hs__shot img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; }
.hs__shot::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(100deg, rgba(10,10,10,.96) 0%, rgba(10,10,10,.92) 38%, rgba(10,10,10,.62) 60%, rgba(10,10,10,.28) 82%, rgba(10,10,10,.18) 100%),
    linear-gradient(to top, rgba(10,10,10,.55) 0%, transparent 42%);
}
/* ── PHONE SCREENSHOTS ──────────────────────────────────────
   A DIFFERENT TREATMENT FROM THE DB THUMBNAILS ABOVE, because the
   source material is a different shape. A portfolio thumbnail is
   landscape and fills the card. A phone capture is 1206x2622 — put
   that through object-fit:cover in a 600x268 slide and you see the
   top 20% of the screen, which is the status bar and a nav title.
   That is not a showcase of anything.

   So the phone is CONTAINED and right-anchored at its true aspect,
   taller than the slide and clipped by .hs__frame's overflow, which
   reads as a device standing behind the copy. No scrim over it: the
   copy is capped at 56% width on the left and the frame plate is
   already dark, so the two never overlap and the screenshot stays
   fully legible. */
.hs__shot--phone {
  inset: auto auto auto auto;
  right: clamp(.9rem, 4%, 2.4rem);
  top: 7%;
  height: 126%;
  width: auto;
  aspect-ratio: 1206 / 2622;
  border: 1px solid rgba(245,241,232,.14);
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 18px 44px rgba(0,0,0,.55);
}
.hs__shot--phone::after { content: none; }
.hs__shot--phone img { object-position: center top; }
/* T-1120: THE DEVICE WAS TOO SMALL TO BE A SCREENSHOT.
   Measured rendered width: 109px at 390 and 120px at 768, against a 560px
   source — a 4.7-5.1x downscale, and 0.28x of the capture's own logical
   iOS width. At that size a screenshot is a coloured rectangle; nobody can
   tell The Dispatch has a map in it. The slide's min-height is what sets
   the device (height is 126% of it), so the two move together. Raising the
   floor from 210 to 268px takes the device to ~155px at 390 and ~168px at
   768 without the hero going anywhere near viewport height — the whole
   section stays around 640px on a phone. */
.hs__slide--shot .hs__link { min-height: clamp(268px, 34vw, 300px); }
/* Narrow slides: the copy column and the device fight for the same
   room, so the device gives way rather than the words — but it gives way
   less than it used to (104% -> 118%), because at 104% it had stopped
   being legible at all and the copy is capped at 58% either way. */
@media (max-width: 620px) {
  .hs__shot--phone { top: 10%; height: 118%; right: .55rem; opacity: .94; }
  .hs__slide--shot .hs__kind,
  .hs__slide--shot .hs__name,
  .hs__slide--shot .hs__desc { max-width: 58%; }
}

/* Keep the copy off the exposed half so it never lands on busy pixels. */
.hs__slide--shot .hs__kind,
.hs__slide--shot .hs__name,
.hs__slide--shot .hs__desc,
.hs__slide--shot .hs__stack { max-width: min(56%, 26rem); }
.hs__slide--shot .hs__kind,
.hs__slide--shot .hs__name,
.hs__slide--shot .hs__desc,
.hs__slide--shot .hs__stack,
.hs__slide--shot .hs__go,
.hs__slide--shot .hs__status { position: relative; z-index: 1; }

/* ── CONTROLS ──────────────────────────────────────────────── */
.hs__bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; margin-top: .8rem;
}
.hs__count {
  margin: 0;
  font-family: var(--font-mono); font-size: .62rem;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(245,241,232,.55);
}
.hs__nav { display: flex; gap: .45rem; }
.hs__btn {
  width: 34px; height: 34px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; cursor: pointer;
  border: 1px solid rgba(245,241,232,.22);
  color: rgba(245,241,232,.8);
  transition: border-color .2s ease, color .2s ease, background .2s ease, transform 120ms cubic-bezier(0.16,1,0.3,1);
}
@media (hover: hover) and (pointer: fine) {
  .hs__btn:hover { border-color: rgba(200,16,46,.6); color: #f5f1e8; background: rgba(200,16,46,.08); }
}
.hs__btn:active { transform: scale(.94); }
.hs__btn:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  /* The track still moves between slides — that is the control doing its
     job and removing it would break navigation — it just moves instantly.
     Auto-advance never starts at all (see js/tech-hero-assembly.js). */
  .hs__track { transition: none !important; will-change: auto; }
  .hs__btn { transition: none !important; }
  .hs__btn:active { transform: none !important; }
}
