/* Shared styling for the three stenokeyboard.app pages. Deliberately small: one file, no build
   step, no external fonts or scripts, so the site can be dropped on any static host. */

/* Dark is the base palette; light applies only when the visitor's OS explicitly asks for it. */
:root {
  --bg: #14161a;
  --fg: #eceef2;
  --muted: #a0a7b4;
  --rule: #2a2e36;
  --accent: #5aa2ff;
  --card: #1c1f25;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #ffffff;
    --fg: #16181d;
    --muted: #5c6370;
    --rule: #e3e6ea;
    --accent: #0a63d6;
    --card: #f6f7f9;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  /* 1.0625rem so this tracks the html font-size below (17px at the default 16px root, unchanged
     from before) instead of a fixed px value that would ignore that media query. */
  font: 1.0625rem/1.65 "Courier New", Courier, monospace;
  -webkit-text-size-adjust: 100%;
}

/* Everything on the page is sized in rem, so bumping the root font-size here scales the whole
   layout together. On a large desktop monitor the default scale reads small at normal viewing
   distance; 125% is the size that looked right. Scoped to wide viewports only, so phones and
   tablets (already handled by their own max-width: 640px tier) are untouched. */
@media (min-width: 1200px) {
  html {
    font-size: 125%;
  }
}

.wrap {
  max-width: 44rem;
  margin: 0 auto;
  padding: 2.5rem 1.25rem 4rem;
}

header.site {
  border-bottom: 1px solid var(--rule);
}

header.site .wrap {
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  /* .brand is itself a flex container (icon + text), so its baseline is derived from whichever of
     those two children participates in baseline alignment; that never quite lines up with the
     plain inline text baseline of the nav links next to it. Centering both blocks on the same axis
     sidesteps the mismatch instead of trying to reconcile two different baselines. */
  align-items: center;
  justify-content: space-between;
}

.brand {
  font-weight: 650;
  font-size: 1.05rem;
  color: var(--fg);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* The real app icon, so the site shows what to look for in the App Store. Sized in rem and set to
   the same corner radius iOS uses, since the source PNG is square. */
.brand img {
  width: 2rem;
  height: 2rem;
  border-radius: 22.5%;
}

nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
  margin-left: 1.25rem;
}

nav a:hover { color: var(--accent); }

h1 {
  font-size: 2.1rem;
  line-height: 1.2;
  margin: 0 0 0.75rem;
}

/* Home page only: an h1 that reads as body copy. The class beats the plain h1 selectors here and in
   the phone block on specificity, so neither size applies to it. Other pages keep the big h1. */
h1.plain {
  font-size: 1rem;
  /* Normal weight, so the only bold words in the opening block are the two product names, which are
     marked up individually. Browsers bold an h1 by default, hence the explicit value. */
  font-weight: 400;
  line-height: 1.65;
  margin: 0 0 0.25rem;
  /* Muted, with the two product names left at full contrast below. Courier New's bold is only
     marginally heavier than its regular and, being monospace, occupies identical width, so weight
     alone did not make the names stand out. Colour does the separating instead. */
  color: var(--muted);
}

h1.plain strong {
  color: var(--fg);
}

/* Matched to it, or the line underneath would be larger than the heading above it. */
h1.plain + .lede {
  font-size: 1rem;
}

h2 {
  font-size: 1.25rem;
  margin: 2.5rem 0 0.5rem;
}

h3 {
  font-size: 1.02rem;
  margin: 1.75rem 0 0.35rem;
}

p, li { color: var(--fg); }

.lede {
  font-size: 1.2rem;
  color: var(--muted);
  margin: 0 0 1.25rem;
}

a { color: var(--accent); }

.button {
  display: inline-block;
  background: var(--accent);
  color: #fff;
  text-decoration: none;
  font-weight: 600;
  padding: 0.7rem 1.4rem;
  border-radius: 10px;
}

.button:hover { filter: brightness(1.08); }

/* Two App Store buttons, one per app. Unused until the listings go live; see index.html. */
.buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 0.75rem;
}

.buttons .button {
  text-align: center;
}

.button-note {
  display: block;
  font-weight: 400;
  font-size: 0.85rem;
  opacity: 0.85;
}

.button.secondary {
  background: transparent;
  color: var(--accent);
  box-shadow: inset 0 0 0 1.5px var(--accent);
}

/* Pulled out sideways by exactly its own padding plus its border, so the text inside the card lines
   up with the body text above and below it instead of sitting indented from the column. */
.card {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 12px;
  padding: 1rem 1.25rem;
  margin: 1.5rem calc(-1.25rem - 1px);
}

/* At that width the card reaches the screen edges on a phone, so square off the corners and drop
   the side borders rather than leaving rounded corners jammed against the edge. */
@media (max-width: 640px) {
  .card {
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    /* No side borders to compensate for here, so the pull-out is the padding alone. Keeping the
       extra 1px would overhang the viewport and make the page scroll sideways. */
    margin-inline: -1.25rem;
  }
}

/* The hero loop sits above the fold and is the one always-visible autoplaying element. Capped
   narrower than the text column so it does not push the App Store buttons down. */
.shots figure {
  margin: 1.5rem 0;
  max-width: 30rem;
}

.shots img,
.shots video {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  border-radius: 12px;
}

/* Video frames are dark captures, so the element shows black rather than page background while
   media loads. Without this there is a pale flash on light-mode machines. */
.shots video {
  background: #000;
}

.hero-demo {
  margin: 0 auto 1.25rem;
  /* Full column width. It used to be capped at 24rem to keep the download buttons on the first
     screen, which stopped mattering once the buttons moved above it. */
  max-width: 100%;
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
}

/* Device frames. Two source assets, cropped tight to their own bezel and measured by hand against
   the actual pixels (see the measuring script used to build img/frame-ipad.png and
   img/frame-iphone.jpg): a screen-shaped hole positioned as a percentage inset from the frame
   image's own edges, sized to the frame's own cropped aspect-ratio so the box never needs a
   pixel value. video/img inside is object-fit: cover, so it only looks right when its own aspect
   ratio is reasonably close to the screen hole's, an authentic capture rather than an arbitrary
   crop: 960x720 (iPad) and 370x800 (iPhone) both are; not every clip on this page is.

   Both frames are flattened onto white and blended onto the video with mix-blend-mode: multiply
   rather than layered with real alpha, even though frame-ipad.png's source did have an alpha
   channel. White is the identity value under multiply (multiplying by white leaves whatever is
   underneath unchanged, so the screen area is invisible and the video shows through cleanly),
   while black always paints black regardless of what's beneath it. The iPad source's alpha
   channel turned out to not be usable directly: part of its bezel band, a glossy highlight along
   one edge, was semi-transparent in the source by design, meant to composite over the designer's
   plain white canvas. Layered directly over a video, that let the video bleed straight through
   the highlight band instead of it reading as bezel. Flattening onto white first and using
   multiply for both frames sidesteps that regardless of what either source asset's alpha does. */
.device-frame {
  position: relative;
  margin: 0 auto;
}

.device-frame__screen {
  position: absolute;
  overflow: hidden;
  background: #000;
}

.device-frame__screen video,
.device-frame__screen img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* .shots video/img (further up this file) puts a border and radius on every clip on the page,
     including this one, since it is a descendant selector and does not care how deeply nested the
     video is. Reset both explicitly rather than rely on this rule coming later in the cascade:
     the frame's own bezel is the visual edge now, a second border here would double up against
     it, and a radius would fight the screen's rectangular clip. */
  border: none;
  border-radius: 0;
}

.device-frame__bezel {
  position: absolute;
  /* inset:0 alone fills the containing block exactly; adding explicit width/height:100% here too
     fought it by a couple of px, since the img is a replaced element with its own intrinsic
     aspect ratio pulling against the percentage resolution. */
  inset: 0;
  pointer-events: none;
}

.device-frame--ipad {
  aspect-ratio: 1200 / 818;
}

.device-frame--ipad .device-frame__screen {
  top: 2.96%;
  right: 1.28%;
  bottom: 2.53%;
  left: 3.15%;
  /* Rounded to roughly match the frame image's own screen corners, so the video's clipped edge
     sits just inside the bezel's curve instead of a sharp rectangular corner poking past it. */
  border-radius: 2.2%;
}

/* The screen hole (measured off the frame photo) is a shade wider than the 4:3 recording, so
   object-fit: cover (the default set above) was cropping a visible sliver off the top and bottom
   of every frame. contain fits the whole recording instead; the sliver of screen area left on the
   sides reads as bezel since .device-frame__screen is already black. */
.device-frame--ipad .device-frame__screen video {
  object-fit: contain;
}

/* frame-ipad.png turned out to need the same treatment as the iPhone one, and for the same
   underlying reason despite starting from a real alpha channel: parts of the bezel band itself
   (a glossy highlight along one edge, not just the screen hole) were semi-transparent in the
   source, by design, meant to be composited over the designer's plain white canvas. Layered
   directly over a video instead of white, that highlight band let the video bleed straight
   through it instead of reading as bezel, visible as a gray, gradient, see-through stripe down
   one side. Now flattened onto white and blended the same way as the phone. */
.device-frame--ipad .device-frame__bezel {
  mix-blend-mode: multiply;
}

.device-frame--iphone {
  aspect-ratio: 800 / 1648;
}

.device-frame--iphone .device-frame__screen {
  top: 1.06%;
  /* Measured right inset came back near 0, an artifact of the source crop rather than real
     bezel geometry (a phone's bezel is symmetric); mirrored from the left measurement instead. */
  right: 2.98%;
  bottom: 1.08%;
  left: 2.98%;
  /* A modern iPhone's corners are noticeably rounder relative to its narrow screen than an
     iPad's are; without this the video's square corners showed past the frame's own curve. */
  border-radius: 9%;
}

.device-frame--iphone .device-frame__bezel {
  mix-blend-mode: multiply;
}

/* The iPad and iPhone clips are matched on height, not width. Giving each a flex-basis of 0 and a
   flex-grow equal to its own aspect ratio makes the widths land in proportion to those ratios,
   which is the same thing as both clips rendering at one shared height. The two together still
   fill the column exactly. Equal widths instead would leave the portrait clip roughly three times
   the height of the landscape one. */
.hero-demo .device-frame {
  /* Keyed to the FRAME's own aspect ratio (bezel included), not the raw video's: the two differ
     slightly (iPad frame 1.4657 vs video 1.333, iPhone frame 0.4853 vs video 0.4625) because of
     the bezel padding, and matching only the video would leave the two frames' heights adrift. */
  flex: 1.4657 1 0;
  min-width: 0;
}

.hero-demo .device-frame + .device-frame {
  flex-grow: 0.4853;
}

.shots figcaption {
  color: var(--fg);
  font-weight: 700;
  font-size: 0.92rem;
  margin-top: 0.5rem;
}

/* Gallery carousel: one clip centered and sharp, its immediate neighbor peeking in on each side,
   smaller/faded/blurred, wrapping around in both directions. An earlier attempt at a carousel here
   (reverted) hijacked wheel and pointer events to drive a hand-rolled scroll position with its own
   easing and parallax, and never reached a working state. A second attempt used native
   overflow-x/scroll-snap with the position read back via IntersectionObserver, which worked, but
   native scrolling has no way to express infinite wraparound or a partially-visible off-track
   neighbor card, both of which this design needs. So position here is JS-owned instead
   (carousel.js keeps one `current` index and renders every slide's transform/opacity/blur from it
   each time it changes) - legitimate this time specifically because nothing else (no scroll
   position) competes with it, so there's nothing for it to drift out of sync with.

   Every card shares one fixed box (width below, media height in the img/video rule) rather than
   sizing to its own clip's aspect ratio the way the old swipe-strip did: the landscape (4:3) and
   portrait (3:4) clips in this gallery are different shapes, and a coverflow needs the center and
   peeking cards to line up as matching rectangles. object-fit: contain below letterboxes whichever
   orientation a given clip is inside that shared box rather than cropping it. */
.carousel {
  position: relative;
  margin: 1.25rem 0 0;
}

.carousel__viewport {
  position: relative;
  overflow: hidden;
  /* Tall enough for the shared card box (media height below, plus its border) and up to a few
     lines of caption underneath, so the tallest real caption never clips. */
  height: 28rem;
  /* Without this, a horizontal drag is ambiguous to the browser (is it a swipe or the start of a
     scroll?), and it can answer that by firing pointercancel instead of letting carousel.js's
     pointerup handler see the gesture through. This tells it horizontal motion here is ours;
     vertical panning still scrolls the page normally. */
  touch-action: pan-y;
}

.carousel__track {
  position: relative;
  height: 100%;
}

.carousel__slide {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(20rem, 100%);
  margin: 0;
  padding: 0 0.25rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* carousel.js adds a second translateX (the coverflow offset) plus scale after this; the -50%
     here is relative to the slide's own width, so it centers regardless of what min() resolved to. */
  transform: translateX(-50%);
  transition: transform 0.4s ease, opacity 0.4s ease, filter 0.4s ease;
}

.carousel__slide img,
.carousel__slide video {
  display: block;
  width: 100%;
  height: 18rem;
  object-fit: contain;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: #000;
  /* A mouse-drag swipe on a plain <img> otherwise starts the browser's native "drag this image"
     operation instead of reaching carousel.js's pointer handlers. */
  -webkit-user-drag: none;
  user-select: none;
}

.carousel__slide figcaption {
  color: var(--fg);
  font-weight: 700;
  font-size: 0.88rem;
  line-height: 1.45;
  margin-top: 0.6rem;
  min-height: 5.5rem;
}

/* Detail kept only on the one slide still using it (dictionaries); set lighter than the caption
   above it so that reads as the label and this as the note under it. */
.carousel__slide figcaption .detail {
  display: block;
  font-weight: 400;
  color: var(--muted);
  margin-top: 0.3rem;
}

@media (prefers-reduced-motion: reduce) {
  .carousel__slide {
    transition: none;
  }
}

/* Positioned just outside the slide edge rather than overlapping it, so the buttons never sit on
   top of the media itself. Never disabled: wrapping is infinite in both directions. */
.carousel__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--card);
  color: var(--fg);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  /* Above the peeking side cards, or the nearer one would sit on top of the button. */
  z-index: 20;
}

.carousel__nav:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.carousel__nav--prev { left: -0.75rem; }
.carousel__nav--next { right: -0.75rem; }

.carousel__dots {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.9rem;
}

.carousel__dot {
  width: 0.5rem;
  height: 0.5rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--rule);
  cursor: pointer;
}

.carousel__dot.is-active { background: var(--accent); }

@media (max-width: 640px) {
  /* The hero pair stacks here. Side by side inside a phone-width column the portrait clip lands
     around 85px wide, where the key bank is no longer legible. Stacked, it is capped to 17rem so
     it does not become a full screen of scrolling before the prose starts. */
  .hero-demo {
    display: block;
    text-align: center;
  }

  .hero-demo .device-frame {
    margin: 0 auto;
  }

  .hero-demo .device-frame + .device-frame {
    height: 17rem;
    width: auto;
    margin-top: 0.75rem;
  }

  h1 {
    font-size: 1.65rem;
  }

  .lede {
    font-size: 1.05rem;
  }
}

.muted { color: var(--muted); }

.small { font-size: 0.92rem; }

footer.site {
  border-top: 1px solid var(--rule);
  margin-top: 3rem;
}

footer.site .wrap {
  padding-top: 1.5rem;
  padding-bottom: 2.5rem;
  color: var(--muted);
  font-size: 0.92rem;
}

footer.site a { color: var(--muted); }

ul { padding-left: 1.25rem; }

li { margin: 0.3rem 0; }

code {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 0.1em 0.35em;
  font-size: 0.9em;
}
