/* =========================================================================
   Wispr Talk — landing page
   =========================================================================

   The page is a note being dictated. Text is left aligned and anchored to the
   top, at one size, the way it would be in a real editor. The pill is a
   one-to-one rebuild of PillView.swift: every number in its section below is
   the point value from that file multiplied by --s.

   Durations here are mirrored by constants at the top of app.js. Change one,
   change the other.
   ========================================================================= */

:root{
  /* Points to pixels. The macOS pill is 34pt tall; a touch over life size is
     enough on a page this empty. */
  --s: 1.45;
  --pt: calc(var(--s) * 1px);

  /* One text size for the whole note. */
  --note: clamp(17.8px, 1.62vw, 23.4px);

  /* Straight off the app icon: #47A8FF at the top, #0088FF at the bottom. */
  --blue:       #0088FF;
  --blue-light: #47A8FF;

  --ink:   #08090A;
  --ink-2: rgba(8, 9, 10, .56);
  --ink-3: rgba(8, 9, 10, .42);

  --paper: #FFFFFF;

  /* The pill on a white page: the app's material and scrim resolve to very
     nearly black, with the hairline highlight still on the edge. */
  --pill:        #141416;
  --pill-stroke: rgba(255, 255, 255, .11);
  --red:         #FF453A;

  /* Custom curves. The built-in ones are too weak to read at this size:
     ease-out for anything entering or leaving, ease-in-out for anything that
     stays on screen and changes shape, and one back-out for the arrival. */
  --out-quart:    cubic-bezier(.25, 1, .5, 1);
  --out-expo:     cubic-bezier(.16, 1, .3, 1);
  --out-back:     cubic-bezier(.34, 1.45, .64, 1);
  --in-out-quart: cubic-bezier(.76, 0, .24, 1);

  /* SF Pro Rounded, via the generic that maps to it on Apple platforms. */
  --font: ui-rounded, "SF Pro Rounded", -apple-system, BlinkMacSystemFont,
          "Segoe UI", system-ui, sans-serif;
}

@media (max-width: 900px) { :root { --s: 1.30 } }
@media (max-width: 620px) { :root { --s: 1.34 } }
@media (max-height: 700px){ :root { --s: 1.25 } }
/* On a short window the note has to start higher and the pill has to sit
   lower, or the longest note runs into the dock. */
@media (max-height: 700px){
  .page{ padding-top: clamp(12px, 9vh, 72px); }
  .dock{ height: clamp(92px, 14vh, 122px); }
}

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

html, body{
  height: 100%;
  margin: 0;
  /* The page is one fixed view. Nothing here scrolls. */
  overflow: hidden;
}

body{
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

/* =========================================================================
   Chrome
   =========================================================================
   Both bars stop at the same width, so on a very wide display the page keeps
   its shoulders instead of pinning the footer to the far corners.
   ========================================================================= */

.chrome{ flex: 0 0 auto; }

.bar{
  width: 100%;
  max-width: 1440px;
  margin: 0 auto;
}

/* The gap above the button and the gap to its right are the same number. */
/* The one exception to the shared width. Chrome pinned to a corner should stay
   in that corner however wide the display gets; it is the footer's contents,
   read as a row, that want shoulders. */
header .bar{
  max-width: none;
  display: flex;
  justify-content: flex-end;
  padding: clamp(14px, 1.7vw, 21px) clamp(22px, 2.5vw, 32px) 0;
}

/* A plain solid button, and a small one. It is the only saturated thing on the
   page, so the less of it there is the better the page reads. */
.cta{
  display: inline-flex;
  align-items: center;
  padding: 10px 17px 11px;
  border-radius: 14px;
  background: linear-gradient(180deg, var(--blue-light), var(--blue));
  color: #fff;
  font: 600 16px/1 var(--font);
  letter-spacing: -.013em;
  text-decoration: none;
  /* Hover lightens it and nothing else; the box never moves. A press is the
     only thing that changes its size. */
  transition: transform 130ms var(--out-quart), filter 180ms ease;
}

@media (hover: hover) and (pointer: fine){
  .cta:hover{ filter: brightness(1.09); }
}

/* After the hover rule, so a press still darkens an already hovered button. */
.cta:active{
  transform: scale(.97);
  filter: brightness(.96);
}

footer .bar{
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 0 clamp(24px, 3.2vw, 44px) clamp(20px, 2.4vh, 30px);
  font-size: clamp(13px, 1.05vw, 14.5px);
  font-weight: 500;
  letter-spacing: -.006em;
  color: var(--ink-3);
}

.f-left{ justify-self: start; display: flex; align-items: center; gap: 11px; }
/* The icon carries its own corners and its own transparency. Nothing to clip. */
.f-left img{ display: block; width: 31px; height: 31px; }
.f-left span{ color: var(--ink); font-weight: 600; }
.f-mid{ justify-self: center; }
.f-right{ justify-self: end; display: flex; align-items: center; gap: 16px; }

/* Underlined, since these are the only clickable things in the footer and
   nothing else here is. The rule replaces the old arrow glyph, which was doing
   the same job less plainly. The underline is set light and dropped clear of
   the baseline so it reads as a link rather than as an edit mark. */
.f-right a{
  color: var(--ink-3);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: rgba(8, 9, 10, .3);
  transition: color 200ms ease, text-decoration-color 200ms ease;
}
.f-right a:hover{ color: var(--ink); text-decoration-color: var(--ink); }

/* =========================================================================
   Stage
   ========================================================================= */

.stage{
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.page{
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  justify-content: center;
  padding: clamp(14px, 16vh, 168px) clamp(22px, 5vw, 56px) 0;
}

/* The caret and the note share one cell and both start at its top left corner,
   so the words appear exactly where the caret was blinking. */
/* Offset rather than centred: the note reads as a document someone is writing
   in, and those sit left of the middle of a window. */
.note{
  position: relative;
  right: clamp(0px, 5vw, 88px);
  width: min(660px, 100%);
  display: grid;
  justify-items: start;
  align-items: start;
}
.note > *{ grid-area: 1 / 1; width: 100%; }

.dock{
  flex: 0 0 auto;
  height: clamp(116px, 19vh, 186px);
  display: grid;
  place-items: center;
}

/* =========================================================================
   The note
   =========================================================================
   One size, one weight, left aligned. Bold exists only where a dictation
   actually produced markdown.
   ========================================================================= */

.script, .idle{
  font-size: var(--note);
  line-height: 1.62;
  letter-spacing: -.011em;
}

.script{ color: var(--ink); }
.script:empty{ display: none; }

.script p{ margin: 0; }
.script p + p,
.script p + ul,
.script ul + p{ margin-top: .95em; }

.script ul{ list-style: none; margin: 0; padding: 0; }
.script li{
  position: relative;
  padding-left: 1em;
}
/* Hung in the margin rather than set inline, so a bullet that wraps keeps its
   second line flush with its first. The hyphen is what Wispr Talk dictated. */
.script li::before{
  content: "-";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--ink-2);
}
.script strong{ font-weight: 600; }

.caret{
  display: inline-block;
  width: .131em;
  min-width: 2.45px;
  /* Taller than the 1.22em it was. The extra height is split between the two
     ends rather than all going upwards, which is what the deeper drop below is
     for. Width is untouched: this is a longer caret, not a heavier one. */
  height: 1.4em;
  border-radius: 999px;
  background: var(--blue);
  vertical-align: -.28em;
  /* A real caret snaps. Steps, not a fade. */
  animation: blink 1.06s steps(1, end) infinite;
}

@keyframes blink{
  0%, 50%      { opacity: 1 }
  50.01%, 100% { opacity: 0 }
}

.idle{ transition: opacity 260ms var(--out-quart); }
.idle.gone{ opacity: 0; }

/* Enter and exit.
   A little blur bridges the gap between one note and the next: without it you
   see two separate blocks swapping, rather than one changing. */
.script > *{ animation: textIn 620ms var(--out-expo) both; }
.script > *:nth-child(2){ animation-delay:  70ms }
.script > *:nth-child(3){ animation-delay: 140ms }
.script > *:nth-child(4){ animation-delay: 210ms }
.script > *:nth-child(5){ animation-delay: 280ms }

/* Dropped once the note has finished arriving. The fill mode is `both`, so the
   last frame stays applied for as long as the note is up, and that frame holds
   a filter. A zero blur is still a filter: it keeps the paragraph on its own
   rasterised layer, everything inside gets resampled, and a 2.4px caret comes
   back softer and fatter than the identical one on the bare page. Ending the
   keyframe on `none` does not help — filter interpolation resolves `none` to
   the identity of the other list, so it computes back to blur(0). Only taking
   the animation off the element clears the layer.

   Before `.script.out`, which carries the same weight and has to win when the
   note leaves. */
.script.settled > *{
  animation: none;
  opacity: 1;
  transform: none;
  filter: none;
}

.script.out > *{
  animation: textOut 340ms var(--out-quart) both;
  animation-delay: 0ms;
}

@keyframes textIn{
  from{ opacity: 0; transform: translateY(9px); filter: blur(6px); }
  60% { filter: blur(0); }
  to  { opacity: 1; transform: none;            filter: blur(0); }
}

@keyframes textOut{
  from{ opacity: 1; transform: none;             filter: blur(0); }
  to  { opacity: 0; transform: translateY(-6px); filter: blur(5px); }
}

/* =========================================================================
   Pill — a rebuild of PillView.swift, in points
   =========================================================================

   contentHeight 22 · controlInset 6 · capsule 34
   iconGap 6 · controlGap 7 · lock gutter 11
   trace 62 x 18, 13 bars, 2 apart
   padding 12/12 unlocked, 8/6 locked

   A plain capsule, drawn by the border radius. Corner smoothing was tried here
   and is the wrong idea: a corner is only smoothed by reaching further along
   both edges, and on a capsule the vertical edge has nothing to give, so any
   amount of it trades the capsule for a rounded rectangle.
   ========================================================================= */

.pill{
  position: relative;
  display: flex;
  align-items: center;
  height: calc(34 * var(--pt));
  padding: calc(6 * var(--pt)) calc(12 * var(--pt));
  border-radius: 999px;
  background: var(--pill);
  box-shadow:
    inset 0 0 0 calc(.5 * var(--pt)) var(--pill-stroke),
    0 calc(2 * var(--pt)) calc(6  * var(--pt)) rgba(0, 0, 0, .16),
    0 calc(6 * var(--pt)) calc(20 * var(--pt)) rgba(0, 0, 0, .10);
  transition:
    padding   420ms var(--in-out-quart),
    transform 300ms var(--out-quart);
  -webkit-user-select: none;
  user-select: none;
}

.pill.locked{
  padding-left:  calc(8 * var(--pt));
  padding-right: calc(6 * var(--pt));
}

/* It is a status indicator, not a control. */
@media (hover: hover) and (pointer: fine){
  .pill:hover{ transform: scale(1.03); }
}

/* Lock — collapses to nothing when the pill is not locked, so the capsule
   stretches rather than jumping between two shapes. */
.lock{
  flex: 0 0 auto;
  width: 0;
  margin-right: 0;
  opacity: 0;
  overflow: hidden;
  color: rgba(255, 255, 255, .58);
  display: flex;
  justify-content: center;
  transition:
    width   420ms var(--in-out-quart),
    margin  420ms var(--in-out-quart),
    opacity 300ms var(--out-quart);
}
/* Sized by height, to match a 12pt SF Symbol rather than to fill the gutter. */
.lock svg{ height: calc(12.6 * var(--pt)); width: auto; flex: 0 0 auto; }

.pill.locked .lock{
  width: calc(11 * var(--pt));
  margin-right: calc(6 * var(--pt));
  opacity: 1;
}

/* Trace */
.trace{
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width:  calc(62 * var(--pt));
  height: calc(18 * var(--pt));
}
.trace i{
  flex: 0 0 auto;
  /* (62 - 12 gaps of 2) / 13 bars */
  width: calc(2.923077 * var(--pt));
  min-height: calc(2 * var(--pt));
  border-radius: 999px;
  background: #fff;
  will-change: height, opacity;
}

/* Stop */
.stop{
  flex: 0 0 auto;
  width: 0;
  margin-left: 0;
  opacity: 0;
  overflow: hidden;
  display: flex;
  transition:
    width   420ms var(--in-out-quart),
    margin  420ms var(--in-out-quart),
    opacity 300ms var(--out-quart);
}
.pill.locked .stop{
  width: calc(22 * var(--pt));
  margin-left: calc(7 * var(--pt));
  opacity: 1;
}
.stop-dot{
  flex: 0 0 auto;
  width:  calc(22 * var(--pt));
  height: calc(22 * var(--pt));
  border-radius: 50%;
  background: var(--red);
  display: grid;
  place-items: center;
}
.stop-dot::before{
  content: "";
  width:  calc(9.4 * var(--pt));
  height: calc(9.4 * var(--pt));
  border-radius: calc(2.2 * var(--pt));
  background: #fff;
}

/* Arrival and departure.
   Two wrappers, because opacity wants a plain ease-out and the travel wants
   the overshoot, and one keyframe track can only carry one curve. */
.pill-fade{ opacity: 0; }
.pill-rise{ transform: translateY(calc(26 * var(--pt))) scale(.94); }

.pill-fade.in{ animation: fadeIn 620ms var(--out-quart) both; }
.pill-rise.in{ animation: riseIn 980ms both; }

.pill-fade.out{ animation: fadeOut 320ms var(--out-quart) both; }
.pill-rise.out{ animation: riseOut 340ms var(--out-quart) both; }

@keyframes fadeIn{
  from{ opacity: 0; filter: blur(6px); }
  55% { filter: blur(0); }
  to  { opacity: 1; filter: blur(0); }
}
/* Damped rather than a single overshoot: it goes past, comes back a little
   short, and settles. One back-out curve can only do the first of those. */
/* Four oscillations, each about a third of the one before it, over a shorter
   run than a single overshoot would need. The decay is what keeps it playful
   rather than rubbery: it is the second and third bounces you read as spring,
   and their getting small fast is what stops it looking like a toy. */
@keyframes riseIn{
  0%  { transform: translateY(calc(26 * var(--pt))) scale(.94);
        animation-timing-function: cubic-bezier(.20, .70, .28, 1); }
  42% { transform: translateY(calc(-4.4 * var(--pt))) scale(1.024);
        animation-timing-function: cubic-bezier(.40, 0, .30, 1); }
  63% { transform: translateY(calc(1.5 * var(--pt))) scale(.992);
        animation-timing-function: cubic-bezier(.40, 0, .30, 1); }
  80% { transform: translateY(calc(-.5 * var(--pt))) scale(1.003);
        animation-timing-function: cubic-bezier(.40, 0, .30, 1); }
  92% { transform: translateY(calc(.16 * var(--pt))) scale(.9991);
        animation-timing-function: var(--out-quart); }
  100%{ transform: translateY(0) scale(1); }
}
@keyframes fadeOut{
  from{ opacity: 1; filter: blur(0); }
  to  { opacity: 0; filter: blur(5px); }
}
@keyframes riseOut{
  from{ transform: translateY(0) scale(1); }
  to  { transform: translateY(calc(13 * var(--pt))) scale(.95); }
}

/* =========================================================================
   Reduced motion — one composed frame, no performance
   ========================================================================= */

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
  .caret{ opacity: 1; }
}

/* =========================================================================
   Narrow screens
   ========================================================================= */

@media (max-width: 560px){
  /* The note. Two things were wrong here. The offset that pulls the note left
     of centre on a desktop was still applied, and at 5vw it ate the whole of
     the page's own 22px gutter, so the caret and the first character sat on the
     edge of the screen. And the top padding, at 16vh of a tall phone, started
     the note lower than it needs to on a screen this narrow, where the same
     words take twice as many lines. Offset off, gutter set outright, top
     raised. */
  .note{ right: 0; }
  /* High on the screen, and hung from the top: the caret has to sit where the
     first word will land, and a centred block moves that line every time the
     note changes length. Kept low enough that the longest note — 368px here —
     still clears the footer, which the three-row footer below makes tighter
     than it was. */
  .page{ padding: clamp(10px, 5vh, 44px) 24px 0; }

  .cta{ font-size: 14.5px; padding: 10px 17px 11px; }

  /* One centred column: icon, copyright, link. The grid is dropped for a
     column flex, so there is nothing left to hold in three columns and the
     stack centres itself. */
  footer .bar{
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Spaced by hand rather than by one gap: the icon wants room from the
       wordmark under it, the link wants to sit close to the line it belongs
       with. */
    gap: 0;
    /* Enough air below the link line that it doesn't read as glued to the
       edge. env() is added on top, not a floor: Safari reports 0 while its
       toolbar is up, which is always here, since the page does not scroll.
       It only pays out if the toolbar goes, and then it keeps the stack off
       the home indicator too. */
    padding: 0 14px calc(18px + env(safe-area-inset-bottom, 0px));
    font-size: clamp(10px, 3.2vw, 13px);
  }

  .f-mid  { margin-top: 10px; }
  .f-right{ margin-top: 7px; }

  .f-left img{ width: 27px; height: 27px; }
  /* The line below the icon already reads "Wispr Talk". */
  .f-left span{ display: none; }
  .f-right{ gap: 12px; }
  .f-mid, .f-right a{ white-space: nowrap; }
}

/* A phone that is short as well as narrow — an SE, or any handset once Safari's
   toolbars are counted — cannot fit the longest note above a three row footer
   at full size. Rather than let it run under the footer, the note gives back
   about 15% of its height and the footer tightens. It has to come after the
   block above to win, both being one class deep. */
@media (max-width: 560px) and (max-height: 620px){
  :root{ --note: 16.4px; }
  .script, .idle{ line-height: 1.5; }
  .page{ padding-top: 10px; }
  .f-mid{ margin-top: 7px; }
  .f-right{ margin-top: 5px; }
}
