/* ---------------------------------------------------------------------------
   THE DEVICE. One iPhone, FIVE surfaces.

   There were five, and they did not look like each other. `/demo` and `/` had
   one apiece (near-identical, drifting); the onboarding landing had a 40px-radius
   rectangle with a blue square for an avatar and its status bar set to
   `display: none`; the dashboard had a NOTCH-era handset whose battery indicator
   was the literal string "\u25AE\u25AE\u25AE 100%"; and the product itself was a rounded
   card with no bezel at all, which is what a visitor met immediately after
   clicking through from the marketing page that shows the real thing.

   Two implementations of an iPhone are two iPhones that stop looking alike a
   radius at a time, and the second one is always the worse one because it is
   drawn from memory of the first. Five is not a design system, it is five
   people's memories of a phone.

   Both static pages (/ and /demo) link this stylesheet: `/device.css`, out of
   `public/`, rather than under `landing-v3/assets/` where it lived when only one
   page drew a phone. (The app that shared it until October 2026 was replaced by
   Lewis v2, which draws no phone.)

   ITS OWN FILE RATHER THAN app.css, and that is load-bearing rather than tidy.
   `scripts/landing-v3-scale.ts` re-emits every length in casa.css AND app.css
   wrapped in `calc(N * var(--v3-scale))`, which is exactly right for the design
   system and exactly wrong here: an iMessage screen is Apple's geometry laid out
   in iOS POINTS at 1:1, and the whole phone is scaled once, by a transform, at
   the end. Scaling it twice would put 17pt body text on a fractional pixel grid,
   where glyph advances round to whole subpixels and every string comes out a few
   percent wide. That error is proportional and no max-width undoes it. The
   generator never reads this file, so nothing here can be scaled by accident.

   Loaded AFTER scale.css on both pages, so it is the last word on the device.

   What is NOT in here: the two pages' own stage and scaling wrappers
   (`.demo-split`, `.stage`, `.dev-slot` on /demo; `.ph-wrap`, `.ph-slot` on /).
   Those are layout decisions about a column, not about a phone, and they differ
   because one page shows two phones and the other shows one.
   --------------------------------------------------------------------------- */

.dev-frame { position: relative; width: 393px; height: 822px; background: #0b0b0d; border-radius: 62px; padding: 5px;
  box-shadow: 0 10px 26px rgba(0,0,0,.18), 0 42px 88px rgba(0,0,0,.26); }
/* `> i`, NOT ` i`. This is the volume rocker and the side button, and as a
   descendant selector it also claimed every <i> anywhere inside the screen: the
   Focus glyph beside the clock came out as a 3px dark sliver pinned to the left
   edge of the phone, which is a very confusing thing to look at. */
.dev-frame > i { position: absolute; width: 3px; background: #2a2a2e; border-radius: 2px; }
.dev-frame > i:nth-of-type(1) { left: -2px; top: 158px; height: 34px; }
.dev-frame > i:nth-of-type(2) { left: -2px; top: 212px; height: 66px; }
.dev-frame > i:nth-of-type(3) { left: -2px; top: 292px; height: 66px; }
.dev-frame > i:nth-of-type(4) { right: -2px; top: 236px; height: 102px; }

/* `--dev-thread-top` / `--dev-thread-bot` are the two insets a LIVE thread sits
   between: enough to clear the glass header, enough to clear the composer. They
   are variables rather than literals because the bare phone below moves both,
   and a consumer that hard-codes 176 renders its first bubble under the header
   the moment the bezel comes off. `.msg-list` keeps its own literals: it is the
   SCRIPTED thread on the two marketing pages, which never goes bare. */
.dev-screen { position: relative; width: 100%; height: 100%; overflow: hidden; border-radius: 57px; background: #fff;
  --dev-thread-top: 176px; --dev-thread-bot: 96px;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  color: #000; -webkit-font-smoothing: antialiased; }
/* Time on the left with the Focus glyph beside it, then the three status
   icons. Measured off a real 402pt screenshot: 17pt semibold, 30pt of side
   padding, and the whole row on the 54pt line the Dynamic Island sits in. */
/* The insets and the height are measured off a 393pt iPhone 15 Pro at 3x, which
   is this screen's own width: the clock's INK starting at 53.33pt, the battery's
   nub ending 32.67pt from the right, and both centred on 29.33pt — the same
   line the Dynamic Island is centred on, which is why the row is 58.67 tall
   rather than a round number.

   The left inset is 52.25 rather than 53.33 because padding positions the text
   BOX and iOS's 53.33 is where the ink lands. Every SF lining digit carries a
   ~1.08pt left side bearing, so subtracting it once is correct for any time
   string rather than for the one the mock happens to show. */
.dev-status { position: absolute; inset: 0 0 auto 0; height: 58.67px; z-index: 12; display: flex; align-items: center;
  justify-content: space-between; padding: 0 32.67px 0 52.25px; font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
.dev-time { display: inline-flex; align-items: center; gap: 6px; }
.dev-time i { font-style: normal; font-size: 15px; line-height: 1; }
/* One SVG carrying its own internal spacing — see the note in device.tsx for
   why a flex `gap` cannot draw this row. */
.dev-ic { display: block; flex: none; }
/* 125 x 36.67 at 11.33 from the top: the real thing on a 393pt phone, not a
   rounded-down guess. Everything in the status bar is centred on its centre. */
.dev-island { position: absolute; left: 50%; top: 11.33px; transform: translateX(-50%); width: 125px; height: 36.67px;
  border-radius: 999px; background: #0b0b0d; z-index: 13; }
.dev-home { position: absolute; bottom: 9px; left: 50%; transform: translateX(-50%); width: 148px; height: 5px;
  border-radius: 999px; background: #111; opacity: .85; z-index: 12; }

/* iOS 26 glass: the translucency is the whole of it. */
/* iOS 26 glass, and THE TRANSLUCENCY IS THE WHOLE OF IT.
   Three layers, and every one is doing work:

   1. `backdrop-filter` blurs what is behind. That is the part everybody
      implements, and on its own it looks like frosted plastic.
   2. The `brightness` and `saturate` in the same filter are what make it read
      as GLASS rather than as a grey panel: real glass lifts and enriches what
      is behind it before it blurs it. Dropping either one is the difference
      between the composer looking like it is floating over the thread and
      looking like it is painted on top of it.
   3. The background is a vertical GRADIENT rather than a flat white, brightest
      at the top edge and slightly brighter again at the bottom. That is the
      specular line along a curved edge, and it is what stops a 44px pill from
      reading as a rounded rectangle with blur behind it.

   The inset hairline is the edge of the glass; the two outer shadows are the
   contact shadow and the ambient one. */
.g {
  background: linear-gradient(180deg, rgba(255,255,255,.80) 0%, rgba(255,255,255,.56) 42%,
                                      rgba(255,255,255,.54) 72%, rgba(255,255,255,.68) 100%);
  backdrop-filter: blur(20px) saturate(155%) brightness(1.08);
  -webkit-backdrop-filter: blur(20px) saturate(155%) brightness(1.08);
  box-shadow:
    inset 0 0 0 .5px rgba(255,255,255,.7),
    inset 0 1px 0 rgba(255,255,255,.92),
    inset 0 -.5px 0 rgba(255,255,255,.55),
    0 8px 22px rgba(0,0,0,.10),
    0 2px 5px rgba(0,0,0,.07);
}

.msg-scroll { position: absolute; inset: 0; overflow: hidden; }
.msg-list { position: absolute; left: 0; right: 0; top: 170px; transition: transform .42s cubic-bezier(.22,.61,.36,1); }

/* A SCRIPTED thread is DRIVEN; a LIVE one is SCROLLED, and the default above is
   the first of those.

   `/`'s hero runs a written conversation on a timer, so its thread is positioned
   by the page (`.ph-thread` hangs it off the composer) and moved by the page. It
   has nothing to scroll back to: every line it will ever show is one it is about
   to play.

   `/demo` is the other kind. It is a real conversation that grows for as long as
   somebody keeps typing, and with the list absolutely positioned and translated
   there was no way to go back and read the top of it -- the first bubbles slid
   up under the glass header and out of reach. A transcript you cannot scroll is
   a transcript that only exists while it fits.

   So this is the opt-in: the SCREEN becomes the scroller and the list goes back
   into normal flow, with the same two insets the transform was reproducing --
   170px clears the header, 96px clears the composer (822 - 726, the anchor the
   old maths used). Bubbles still pass UNDER both, because the header and the
   composer are later siblings painting over this box rather than edges it stops
   at.

   The bar is hidden on purpose. It would be the one piece of desktop furniture
   on a surface whose whole job is to look like a phone. */
.msg-scroll--live { overflow-y: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.msg-scroll--live::-webkit-scrollbar { width: 0; height: 0; }
.msg-scroll--live > .msg-list {
  position: static; top: auto; transform: none; transition: none;
  padding-top: 170px; padding-bottom: 96px;
}
.msg-row { margin-top: 3px; }
.msg-row.turn { margin-top: 9px; }
.msg-meta { text-align: center; color: #8E8E93; font-size: 12.5px; line-height: 15.5px; letter-spacing: -.01em; }
.msg-out { padding-left: 70px; padding-right: 16px; display: flex; justify-content: flex-end; }
.msg-in  { padding-left: 16px; padding-right: 70px; display: flex; }

/* The tail as two rectangles rather than a bezier: it survives being drawn
   half a pixel wide, which a curve does not. */
.b { position: relative; max-width: 262px; }
.b > span { position: relative; z-index: 2; display: block; border-radius: 18px; padding: 8.5px 13.5px 9.5px;
  font-size: 17px; line-height: 21.5px; letter-spacing: -.022em; word-break: break-word; white-space: pre-line; }
.b.out > span { background: #248BF5; color: #fff; }
.b.in  > span { background: #E9E9EB; color: #000; }
.b::before, .b::after { content: ""; position: absolute; bottom: 0; }
.b.out::before { right: -5px; width: 14px; height: 15px; background: #248BF5; border-bottom-left-radius: 11px; z-index: 0; }
.b.out::after  { right: -7px; width: 7px;  height: 15px; background: #fff;    border-bottom-left-radius: 8px;  z-index: 1; }
.b.in::before  { left: -5px;  width: 14px; height: 15px; background: #E9E9EB; border-bottom-right-radius: 11px; z-index: 0; }
.b.in::after   { left: -7px;  width: 7px;  height: 15px; background: #fff;    border-bottom-right-radius: 8px;  z-index: 1; }
.b.fresh { animation: pop .26s cubic-bezier(.22,.61,.36,1); }
@keyframes pop { from { opacity: 0; transform: translateY(9px) scale(.9) } to { opacity: 1; transform: none } }

/* THE BOTTOM BAND IS ALMOST GONE, and that is what makes the composer glass.
   It used to paint an opaque white gradient over the bottom 108px, which meant
   the pill was sitting on white and `backdrop-filter` had nothing to filter:
   the most expensive part of the chrome was rendering a very slightly grey
   rectangle. In iOS the thread runs UNDER the composer and you read it through
   the glass, smeared and lifted, and that refraction is most of what tells you
   the bar is floating. So the band now fades only the last few pixels above the
   home indicator, and the messages carry on behind the pill.

   The top band stays opaque, because the header is doing something different:
   it is a solid surface with the status bar on it, and a message sliding out
   from under the time is wrong in the other direction. */
.band-top, .band-bot { position: absolute; left: 0; right: 0; z-index: 8; pointer-events: none; }
.band-top { top: 0; height: 176px; background: linear-gradient(to bottom,#fff 0%,#fff 55%,rgba(255,255,255,.9) 74%,rgba(255,255,255,0) 100%); }
/*
  The fade ENDS where the composer begins -- 44px, `.cmp-bar`'s own `bottom`.

  It was 30, which left a 14px strip between the two where a bubble scrolled
  past in full focus, right against the home indicator. Nothing reached that
  strip while the thread was translated rather than scrolled, because the list
  stopped at the anchor; a real scroller goes all the way under.

  It may not grow past 44. The composer occupies 44..86 and its glass has to have
  something to filter -- painting white behind it is how `backdrop-filter` was
  turned into grey plastic once already (the band was 108px then). Below 44 there
  is no glass, only bare screen, so a fade there costs nothing and is the only
  thing standing between a scrolled bubble and the bottom bezel.
*/
.band-bot { bottom: 0; height: 44px; background: linear-gradient(to top,#fff 0%,rgba(255,255,255,.75) 45%,rgba(255,255,255,0) 100%); }

.hdr-back { position: absolute; left: 16px; top: 62px; width: 81px; height: 41px; border-radius: 20.5px; display: flex;
  align-items: center; gap: 8px; padding: 0 8px 0 11px; z-index: 9; }
/* Without an unread count there is nothing in the pill but the chevron, and a
   81px lozenge with a 7px glyph rattling around in it reads as a control that
   failed to load. iOS draws the bare chevron on its own. */
.hdr-back:not(:has(em)) { width: 42px; justify-content: center; padding: 0; }
.hdr-back em { background: #1C1C1E; color: #fff; border-radius: 11px; font-size: 15.5px; font-weight: 600; line-height: 22px;
  padding: 0 8px; letter-spacing: -.02em; font-style: normal; }
.hdr-ft { position: absolute; right: 16px; top: 62px; width: 42px; height: 42px; border-radius: 21px; display: grid; place-items: center; z-index: 9; }
.hdr-av { position: absolute; left: 50%; top: 58px; margin-left: -30px; width: 60px; height: 60px; border-radius: 999px; overflow: hidden; z-index: 9;
  background: #E9E9EB; display: grid; place-items: center; font-size: 26px; }
.hdr-av img { width: 100%; height: 100%; display: block; }
.hdr-name { position: absolute; left: 50%; top: 113px; transform: translateX(-50%); height: 31px; padding: 0 12px; border-radius: 15.5px;
  display: flex; align-items: center; justify-content: center; gap: 5px; z-index: 9; font-size: 16.5px; font-weight: 700;
  letter-spacing: -.035em; white-space: nowrap; }
/* The disclosure chevron is grey, not ink: it is an affordance rather than part
   of the name, and at this weight a black one reads as punctuation.

   The selector has to reach the PATH. `.hdr-name svg` only sets the property on
   the <svg>, which the path then inherits — and an inherited value loses to the
   path's own `stroke="#000"` presentation attribute, so the rule was there and
   the chevron was black anyway. Matching the path directly is what actually
   outranks the attribute. */
.hdr-name svg path { stroke: #8E8E93; }

/* The two pills that can be BUTTONS rather than pictures (see device.tsx).
   A <button> inherits neither the screen's font nor its colour, so without this
   the header renders in the UA's 13px Arial the moment either one is wired up.
   `appearance: none` is what stops iOS drawing its own rounded rectangle over
   the glass. */
button.hdr-back, button.hdr-name { -webkit-appearance: none; appearance: none; border: 0; margin: 0;
  font: inherit; color: #000; cursor: pointer; -webkit-tap-highlight-color: transparent; }
button.hdr-name { font-size: 16.5px; font-weight: 700; letter-spacing: -.035em; }
button.hdr-back:active, button.hdr-name:active { transform: scale(.96); }
button.hdr-name { transition: transform .12s ease; }
button.hdr-back { transition: transform .12s ease; }
/* Open: the disclosure lies down, the way every iOS disclosure does. The
   transform is written out in full because the closed state is an INLINE
   `scaleX(-1)` on the two static pages' markup, and a rule that only rotated
   would be overridden by it. */
button.hdr-name[aria-expanded="true"] svg { transform: scaleX(-1) rotate(-90deg) !important; }
.hdr-name svg { transition: transform .18s cubic-bezier(.22,.61,.36,1); }

/* Two floating pieces rather than one bar: a glass circle for the attachment
   button and a glass pill for the field, with real space between them and real
   space at the edges. The insets are wider than the header's for the same
   reason they are on the device: the composer floats over the thread and the
   header is a surface the thread goes under. */
.cmp-plus { position: absolute; left: 22px; bottom: 44px; width: 42px; height: 42px; border-radius: 21px; display: grid; place-items: center; z-index: 9; }
.cmp-bar { position: absolute; left: 74px; right: 22px; bottom: 44px; height: 42px; border-radius: 21px; display: flex; align-items: center; gap: 8px; padding: 0 6px 0 16px; z-index: 9; }
/* The dictation glyph, which is what is in the field until you type something.
   It swaps for the send button on the same flag the send button already uses,
   so the two can never both be there. */
.cmp-mic { flex: none; width: 30px; height: 30px; display: grid; place-items: center; color: #8E8E93; transition: opacity .16s ease; }
.cmp-bar[data-typed="true"] .cmp-mic { display: none; }
.cmp-bar input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font: inherit; font-size: 17px; letter-spacing: -.022em; color: #000; }
.cmp-bar input::placeholder { color: #8E8E93; }
.cmp-bar input:disabled { opacity: .5; }
.cmp-send { width: 30px; height: 30px; border: 0; border-radius: 999px; background: #248BF5; color: #fff; display: grid; place-items: center;
  cursor: pointer; flex: none; opacity: 0; pointer-events: none; transition: opacity .16s ease; }
.cmp-bar[data-typed="true"] .cmp-send { opacity: 1; pointer-events: auto; }

.typing { display: inline-flex; align-items: center; gap: 5px; background: #E9E9EB; border-radius: 17px; padding: 11px 15px; }
.typing i { width: 8.5px; height: 8.5px; border-radius: 999px; background: #9E9EA3; animation: dot 1.15s infinite ease-in-out; }
.typing i:nth-child(2) { animation-delay: .18s }
.typing i:nth-child(3) { animation-delay: .36s }
@keyframes dot { 0%,60%,100% { opacity:.38; transform: translateY(0) } 30% { opacity:1; transform: translateY(-8%) } }

/* --------------------------------------------------------------------------
   The two shapes a phone comes in

   FIXED is the marketing shape and lives on the two static pages: 393x822 iOS
   points, transform-scaled, with the caller reserving the scaled box, because a
   transform does not change the space an element takes and a scaled phone on
   its own leaves a 393x822 hole in a grid. Those pages declare their own
   `.dev-slot` / `.dev` because each answers "how big is it in this column"
   differently; the pair below is the same arithmetic for React callers.

   FLUID is the product shape. Every piece of chrome in this file is anchored to
   a top or a bottom edge, so the only thing a different height changes is how
   much thread fits, which is what a working tool wants and a picture of a phone
   does not. It is what the dashboard and /demo/* use, and it is why the product
   can have the real bezel without losing the screen to it.
   -------------------------------------------------------------------------- */
.dev-slot { --s: 1; position: relative; width: calc(393px * var(--s)); height: calc(822px * var(--s)); flex: none; }
.dev-slot > .dev { position: absolute; top: 0; left: 0; width: 393px; height: 822px; transform: scale(var(--s)); transform-origin: top left; }

/* `flex: 1` and not just `width: 100%`. A fluid phone is almost always the tall
   thing in a flex column (a toggle above it, the phone filling what is left), and
   a flex item with no basis takes its content's height: the frame came out ten
   pixels tall, which is `.dev-island` and nothing else. */
.dev-fluid { display: flex; justify-content: center; width: 100%; flex: 1 1 auto; min-height: 0; }
.dev-frame--fluid { width: min(100%, 402px); height: 100%; min-height: 0; }
/* `min-height: 0` all the way down, because these sit inside flex columns and a
   flex item's default `min-height: auto` refuses to shrink below its content:
   without it the phone grows past the viewport and the page scrolls instead of
   the thread. */
.dev-frame--fluid > .dev-screen { width: 100%; height: 100%; }

/* --------------------------------------------------------------------------
   THE PHONE, ON A PHONE: `dev-frame--mobile`

   On a laptop the bezel is the point. It is what tells a visitor that the
   thing they are reading is a text message thread rather than a chat widget.
   On an actual phone it is the opposite: a second Dynamic Island under the real
   one, a painted 2:46 above the real clock, a 62px radius eating the corners of
   a screen that has its own, and the whole conversation squeezed into 340 of
   the 390 points the browser already gave us. The frame stops being a signal
   and starts being a smaller screen with a drawing of a screen on it.

   So below the breakpoint the BEZEL goes and the MESSAGES stay. Everything
   removed here is iPhone (frame, buttons, island, status bar, home indicator);
   everything kept is iMessage (glass header, bubbles, glass composer), which is
   the half that was ever the product.

   It is a media query rather than a prop because the alternative is asking
   JavaScript how wide the window is: that renders the desktop shape on the
   server, swaps it after hydration, and the flash lands on the one surface
   whose whole job is to not look like a web page.

   The query is duplicated in `v2-chat.css`. Change one, change both.
   -------------------------------------------------------------------------- */
@media (max-width: 780px), (max-height: 620px) {
  .dev-frame--mobile { width: 100%; height: 100%; padding: 0; border-radius: 0;
    background: transparent; box-shadow: none; }
  .dev-frame--mobile > i { display: none; }
  .dev-frame--mobile .dev-island,
  .dev-frame--mobile .dev-status,
  .dev-frame--mobile .dev-home { display: none; }

  /* The header block shifts up by the 50pt the status bar was holding, keeping
     its own internal geometry (avatar over name, both centred) exactly as iOS
     draws it. The bottom is `env(safe-area-inset-bottom)` rather than the
     home-indicator gap: on a real phone that inset IS the home indicator, and
     on one without a notch it is 0 and the composer sits where a composer sits.
   */
  .dev-frame--mobile > .dev-screen {
    border-radius: 0;
    --dev-thread-top: 126px;
    --dev-thread-bot: calc(64px + env(safe-area-inset-bottom, 0px));
  }
  .dev-frame--mobile .band-top { height: 126px; }
  .dev-frame--mobile .hdr-back,
  .dev-frame--mobile .hdr-ft { top: 12px; }
  .dev-frame--mobile .hdr-av { top: 8px; }
  .dev-frame--mobile .hdr-name { top: 63px; }

  .dev-frame--mobile .cmp-plus,
  .dev-frame--mobile .cmp-bar { bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
  .dev-frame--mobile .band-bot { height: calc(12px + env(safe-area-inset-bottom, 0px)); }

  /* iOS zooms the page when a font-size under 16px takes focus, and it does not
     zoom back out. The composer's field is already 17px; this is the belt for
     any browser that inherits a smaller one. */
  .dev-frame--mobile .cmp-bar input { font-size: 17px; }
}

/* A phone held SIDEWAYS is 390 points tall, and the portrait header (a 60pt
   avatar with the name under it) eats a third of that before a single bubble
   is drawn. iOS collapses the same header in landscape: the avatar goes and the
   name moves up onto the row the back chevron is already on. This has to come
   AFTER the block above, which sets the portrait offsets it is overriding. */
@media (max-height: 620px) {
  .dev-frame--mobile .hdr-av { display: none; }
  .dev-frame--mobile .hdr-name { top: 12px; }
  .dev-frame--mobile .band-top { height: 66px; }
  .dev-frame--mobile > .dev-screen { --dev-thread-top: 66px; }
}
