/* ---------------------------------------------------------------- phone
 *
 * The room on a phone, which is a different shape of problem from the room on
 * a desktop or an iPad and was being solved with the same stylesheet.
 *
 * On an iPad there is room for the video, the xp card, the chat header, her
 * control strip and the chat all at once. On a 390x780 phone those four bands
 * of furniture came to 460px and left the conversation 282px, and that was the
 * good case: opening the keyboard took the chat log to 28px, and opening the
 * sticker picker took it to 28px *and* pushed the text box and the send button
 * off the bottom of the screen.
 *
 * So here the chat is the room and the video is a guest: a small tile floating
 * in a corner that taps up to full width, gets out of the way by itself when
 * the keyboard or the picker needs the space, and is not drawn at all when
 * there is nothing playing. Everything below is inside one breakpoint and
 * nothing outside it is touched.
 *
 * 700px rather than the 900px used elsewhere in app.css, deliberately: iPad
 * mini portrait is 744px and the stacked layout is right there. This is for
 * things you hold in one hand. The second half of the query is the same phone
 * turned sideways, which is 844x390 — wider than any portrait breakpoint and
 * shorter than any tablet — so that rotating never changes which layout you
 * are in.
 */

@media (max-width: 700px), (max-height: 500px) and (max-width: 950px) {
  /* ------------------------------------------------------------ the page */

  /* Edge to edge. A 16px margin and a drop shadow around a card that fills the
     screen is a card you cannot see the edges of anyway, and it costs 32px of
     width on the narrowest device we have.

     The rows stay as app.css had them. Only the tile being lifted out of the
     flow collapses them to one, and that is written on the class rather than
     here so that a phone whose phone.js never ran still gets the old stacked
     layout instead of a video crushed into a single row. */
  .app {
    position: relative;
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr);
    padding: 0;
    gap: 0;
  }

  body.vid-mini .app { grid-template-rows: minmax(0, 1fr); }

  .panel {
    border-radius: 0;
    box-shadow: none;
    background: var(--card);
  }

  /* ------------------------------------------------------------- header */

  /* The notch. The page is edge to edge now, so the top inset stops being the
     browser's problem and becomes ours. */
  .chat-head {
    position: relative;
    gap: 6px;
    padding: 10px 10px 11px;
    padding-top: max(10px, env(safe-area-inset-top));
  }

  .chat-title { font-size: 15px; }

  /* Three facts and four buttons do not fit on 390px of header. The one that
     goes is "connected", which is the state it is in almost always and which
     says nothing when it is true — the other two are the ones she is reading
     the line for. Anything other than healthy still shows. */
  #conn.ok { display: none; }

  .chat-sub {
    font-size: 11.5px;
    gap: 6px;
    min-width: 0;
    overflow: hidden;
  }

  .chat-sub > * { min-width: 0; }

  /* Whichever is left, most urgent first, and truncating rather than running
     under the buttons. */
  .live-hint { order: -2; }
  .ghost-line {
    order: -1;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The xp card is gone (below); this is what is left of it. A hairline along
     the bottom edge of the header, filling left to right as you level up. It
     costs no layout height at all, which is the entire point. */
  .xp-track.in-head {
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 2px;
    margin: 0;
    border-radius: 0;
    background: transparent;
    z-index: 1;
  }

  /* Four icon buttons and a title do not fit at 375px once the buttons are
     touch sized, and the title is the one that can afford to give.

     Six of them now — the pencil and the mute button joined the row — so they
     give a little too: 34px is still a comfortable target and it is what keeps
     the ghost clock beside the title rather than under it. The layout cycle is
     hidden here (below), so what is actually on screen is five. */
  .chat-head .icon-mini {
    width: 34px;
    height: 34px;
    font-size: 15px;
  }

  /* The title gives first, and gives with an ellipsis rather than by wrapping
     the header into two rows. */
  .chat-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Three, now. The layout cycle is a bigger-screen control: everything it
     offers already exists here in a form that fits one hand — the tile is
     video-and-chat, tapping it is video, and with nothing playing the stage
     takes itself away, which is chat only. What it added instead was a trap.
     Video only hides the chat panel, and this button with it; the other copy
     rides on the video, and the video is where a phone has the least room to
     put anything. room.js keeps a phone in split view to match. */
  .layout-btn { display: none; }

  /* One less button in the corner to sit beside. */
  .viewers { right: 62px; }

  /* ------------------------------------------------------- the xp card */

  /* 60px plus a gap for a progress bar that is looked at once a day. The bar
     itself moves into the header, the numbers move into a chip on the strip.

     Keyed on the class phone.js sets rather than the breakpoint alone, so that
     if the module never loads the card is still there and the level is still
     readable, just in the old place. */
  body.phone .xp { display: none; }

  /* ------------------------------------------------------------ the strip */

  /* Her controls. Five chips is more than fits, and before this the last two
     sat past the right edge with nothing to say they were there — a scrolling
     row with no lip reads as a row that ends where the screen ends.

     The mask is what fixes that: the strip fades out at whichever edge it can
     still be scrolled towards, so there is always a visible hint of more. The
     classes are written by phone.js on scroll; both are set at rest when the
     content does not overflow, which cancels the mask out entirely. */
  .strip {
    gap: 6px;
    padding: 7px 10px;
    scroll-snap-type: x proximity;
    scroll-padding-left: 10px;
  }

  .strip .chip { scroll-snap-align: start; }

  .strip:not(.at-start):not(.at-end) {
    mask-image: linear-gradient(90deg, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
  }

  .strip.at-start:not(.at-end) {
    mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 22px), transparent 100%);
  }

  .strip:not(.at-start).at-end {
    mask-image: linear-gradient(90deg, transparent 0, #000 22px, #000 100%);
  }

  /* Thumb sized without the coarse-pointer query, which a phone in a desktop
     browser does not match and which therefore cannot be tested. */
  .strip .chip {
    padding: 9px 13px;
    font-size: 12.5px;
  }

  /* The level, where the strip already is, so it costs a chip rather than a
     row. Tapping it opens the medals sheet the xp card used to. */
  .chip-xp {
    background: linear-gradient(135deg, var(--lav-soft), var(--pink-soft));
    border-color: transparent;
    color: var(--pink-deep);
  }

  /* ------------------------------------------------------- the video tile */

  /* Mini: a picture-in-picture tile floating over the top of the chat, out of
     the layout entirely so that neither the keyboard nor the picker has to
     negotiate with it. Dragging it snaps it to whichever corner is nearest and
     that choice is remembered, because the one thing it will reliably cover is
     the one thing you wanted to read. */
  body.vid-mini .stage {
    position: absolute;
    z-index: 12;
    top: var(--tile-top, auto);
    right: var(--tile-right, auto);
    bottom: var(--tile-bottom, auto);
    left: var(--tile-left, auto);
    width: 124px;
    gap: 0;
    flex: 0 0 auto;
    transition: top 0.22s ease, right 0.22s ease, bottom 0.22s ease, left 0.22s ease;
    touch-action: none;
  }

  body.vid-mini .stage.dragging {
    transition: none;
    /* Lifted, so it reads as picked up rather than stuck. */
    transform: scale(1.06);
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34);
    border-radius: 14px;
  }

  body.vid-mini .video-wrap {
    aspect-ratio: 4 / 3;
    max-height: none;
    width: 100%;
    margin: 0;
    border-radius: 14px;
    box-shadow: 0 6px 20px rgba(43, 29, 37, 0.34);
    border: 2px solid var(--card);
  }

  /* Cropped, not letterboxed, and this is the whole difference between a tile
     you can watch and one you cannot.

     He streams from a phone, so the picture is portrait. Letterboxing a 9:16
     source into a small landscape box spends almost all of it on black bars and
     leaves a picture about forty pixels wide — which is what "hard to see the
     livestream and the chat at the same time" actually looks like. Filling the
     tile instead loses the top and bottom of the frame, which on a person on
     camera is ceiling and lap, and shows his face at the full width of the
     tile.

     Expanded is the other way round: there the whole frame is the point. */
  body.vid-mini .video-wrap video { object-fit: cover; }

  /* Nothing that needs a 40px target belongs on a 124px tile. The whole tile
     is the button, and it expands. */
  body.vid-mini .fs-btn,
  body.vid-mini .viewers,
  body.vid-mini .love-bar,
  body.vid-mini .go-btn,
  body.vid-mini .controls,
  /* A corner inside a 124px corner is 32px of face. The tile is one picture,
     and tapping it expands to the one that has room for both. Whichever video
     wears the class, not whichever element it is: they trade places. */
  body.vid-mini .video-wrap video.corner,
  body.vid-mini .status { display: none; }

  /* viewer.js drives this one with an inline style as tracks come and go, so
     the override has to outrank it. */
  body.vid-mini .offline { display: none !important; }

  /* Nothing playing, on her page: no tile at all. A 124px black rectangle that
     says nothing is still a rectangle covering the conversation, and the header
     already carries the fact in a line of text. His page keeps the stage docked
     instead — see phone.js — because Go Live lives in it. */
  body.vid-off .stage { display: none; }

  /* It arrives when he does, which is the only moment it matters. */
  body.vid-mini:not(.vid-off) .stage { animation: tile-in 0.34s cubic-bezier(0.2, 1.4, 0.4, 1); }

  @keyframes tile-in {
    from { opacity: 0; transform: scale(0.7); }
    to { opacity: 1; transform: scale(1); }
  }

  /* The other half of hiding the tile: the room still has to say why. */
  .live-hint { font-weight: 800; white-space: nowrap; }
  .live-hint.on { color: var(--pink-deep); }

  body.vid-mini .badge-live {
    top: 6px;
    left: 6px;
    padding: 3px 7px;
    font-size: 9px;
    gap: 4px;
  }

  body.vid-mini .badge-live::before { width: 5px; height: 5px; }

  /* The affordance, and it has to exist in both directions. A tile that grows
     when you tap it but has no way back is a one-way door: the only remaining
     escape was the layout button, which cycles through video-only on the way
     and looks nothing like "undo". */
  .tile-grip {
    position: absolute;
    right: 5px;
    bottom: 5px;
    z-index: 3;
    display: none;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    /* No backdrop-filter here, and none on anything else that sits over the
       picture: blurring live video every frame is what a phone cannot afford.
       See the note at the bottom of app.css. A more solid background instead. */
    background: rgba(43, 29, 37, 0.78);
    color: #ffe4ef;
    font-size: 12px;
    line-height: 1;
    place-content: center;
  }

  body.vid-mini .tile-grip,
  body.vid-full .tile-grip { display: grid; }

  /* Except while the stage is pinned open because Go Live is in it. */
  body.vid-locked .tile-grip { display: none; }

  /* Full width means room for a real target, and it sits clear of the reaction
     bar along the bottom middle. */
  body.vid-full .tile-grip {
    right: 10px;
    bottom: 10px;
    width: 38px;
    height: 38px;
    font-size: 15px;
  }

  /* Full: back in the flow at the top of the room, 16:9 rather than the old
     16:10 at 45dvh, which on a phone was a 234px band of letterboxing. */
  body.vid-full .stage {
    position: static;
    flex: 0 0 auto;
    gap: 8px;
    padding: 8px 8px 0;
  }

  body.vid-full .app { grid-template-rows: auto minmax(0, 1fr); }

  /* Sized from the stream, not from a guess.

     A fixed 16:9 box around a portrait phone camera is mostly black: the box
     takes 38dvh of the screen and gives back a picture a third of that. Driving
     the box from `--vid-ar` (written by phone.js from videoWidth/videoHeight)
     means the height budget is spent on picture — a portrait stream becomes a
     tall narrow window with the chat beside it rather than a wide black one
     with a stamp in the middle.

     Height first, width derived, so the cap is what it says it is whichever
     way round the camera is held. */
  body.vid-full .video-wrap {
    height: 40dvh;
    aspect-ratio: var(--vid-ar, 16 / 9);
    width: auto;
    max-width: 100%;
    margin: 0 auto;
    border-radius: 14px;
  }

  /* Nothing playing has no aspect to follow, and a tall empty box is a worse
     way to say "nothing on" than a short one. */
  body.vid-full.vid-idle .video-wrap {
    height: auto;
    aspect-ratio: 16 / 9;
    width: 100%;
  }

  /* Short screens are landscape phones, where the height budget is the scarce
     one and the stream is usually landscape too. */
  @media (max-height: 560px) {
    body.vid-full .video-wrap { height: 52dvh; }
  }

  /* The host's controls, before he is live. The stage is pinned open then, and
     Go Live is the whole point of the page, so it gets to be a real button. */
  body.vid-full .go-btn { padding: 13px; font-size: 15px; }
  body.vid-full .controls { gap: 6px; }
  body.vid-full .controls button { flex: 1 1 auto; padding: 10px 8px; font-size: 12.5px; }
  body.vid-full .status:empty { display: none; }

  /* Once he is live they are chips in the strip instead — see phone.js. This
     is the 95px that buys back: a full width button and a row of three, both
     of them things he touches about twice a stream, sitting on top of the chat
     he is reading the whole time. */
  body.host-live .go-btn,
  body.host-live .controls { display: none; }

  /* His tile is a monitor, not a preview, and that changes both its shape and
     how the picture sits in it.

     Hers crops to fill, because she is looking at a person and the bars are
     wasted space. His must not: he is checking whether they are both in shot,
     and a tile showing the middle of the frame is a tile that cannot answer
     that question. So it takes the stream's own aspect and letterboxes nothing
     away — what he sees is what is being recorded.

     132px keeps it under the 180px at which cast.js decides the video area is
     somewhere the two of them can go and stand. */
  body.host-live.vid-mini .stage { width: 132px; }
  body.host-live.vid-mini .video-wrap { aspect-ratio: var(--vid-ar, 4 / 3); }
  body.host-live.vid-mini .video-wrap video { object-fit: contain; }

  .chip-cam { font-weight: 800; }
  .chip-cam.off { opacity: 0.55; }

  /* The one control here that is ever urgent. A flat red rather than the pink
     gradient the big Go Live button uses: this is a 12.5px label, and white on
     the light end of that gradient is about 3:1. Flat #b8322e is 5.9:1 and
     reads the same in both themes. */
  .chip-stop {
    background: #b8322e;
    border-color: #d6544e;
    color: #fff;
  }

  .chip-stop:disabled { opacity: 0.6; }

  /* ------------------------------------------------------------- the log */

  .chat-log { padding: 12px 10px; gap: 9px; }
  .msg { max-width: 92%; }

  /* The floor that stops the two bugs this file exists for. The composer is
     allowed to shrink and the picker inside it is allowed to shrink to nothing,
     but the conversation always keeps a strip of itself on screen. Without it
     the log is what flexbox takes the space from, because it is the only child
     that can give. */
  .chat-body { min-height: 84px; }

  /* ---------------------------------------------------------- the composer */

  /* `flex: 0 1 auto` with no min-height, deliberately.

     It has to be allowed to shrink, or the picker cannot give its space back to
     the chat. But it must not be allowed to shrink below what is inside it:
     an explicit `min-height: 0` here let flexbox squash the whole composer to
     46px and clip the text box and the send button off the bottom of the
     screen. Leaving min-height at `auto` resolves it to the min-content size,
     and since the picker below is the one part that may shrink to nothing, that
     floor is exactly "the emoji row, the text box and send". */
  .composer {
    flex: 0 1 auto;
    gap: 7px;
    padding: 8px 8px;
    padding-bottom: max(8px, env(safe-area-inset-bottom));
  }

  /* Everything you can lose a finger to stays exactly the size it is. Only the
     picker gives, and its grid already scrolls. */
  .composer-row,
  .thumb-row,
  .reply-chip { flex: 0 0 auto; }

  /* The closed shape, and the floor the composer keeps to. Only the geometry
     below moves; this is what the panel is when nothing has opened it. */
  .picker {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
    max-height: 52dvh;
  }

  /* -------------------------------------------------------- the picker sheet
   *
   * Open, it is the room. Inside the composer it could never be anything else
   * than tiny. With the keyboard up, which is where you are the moment you
   * search for a sticker, the 444px that is left of a 390x780 phone went to
   * the header, the strip, the chat's 84px floor and the two rows of the
   * composer, and the grid got 131px. That is one row of thumbnails and a
   * sliver of the next, with no way to see what any of them are.
   *
   * So while it is open it leaves the composer's flexbox entirely and stands on
   * the keyboard instead: fixed, from under the header down to `--kb` (measured
   * in room.js, because iOS does not shrink the layout viewport for a
   * keyboard). That is 236px of grid with the keyboard up and 533px without,
   * and the header stays visible so the room is still behind it. Measured with
   * scripts/picker-phone-test.html, which is also where to check a change here.
   */
  .picker.open {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--kb, 0px);
    top: calc(56px + env(safe-area-inset-top));
    z-index: 14;
    height: auto;
    max-height: none;
    gap: 7px;
    padding: 8px 10px;
    padding-bottom: max(8px, env(safe-area-inset-bottom));
    border: none;
    border-radius: 22px 22px 0 0;
    background: var(--card);
    box-shadow: 0 -14px 40px rgba(43, 29, 37, 0.32);
    animation: sheet-up 0.26s cubic-bezier(0.2, 0.9, 0.3, 1);
    /* Written by the drag handler in phone.js, the same way the sheets do it. */
    transform: translateY(var(--sheet-y, 0));
  }

  .picker.open.settling { transition: transform 0.22s cubic-bezier(0.3, 0.9, 0.3, 1); }

  /* Landscape is 390px tall, and with a keyboard in it about 200px. There is
     nothing to leave room for up there, the mood chips are a shortcut the
     search box already offers, and the cells want to be smaller rather than
     bigger: the screen is 844px wide, so a smaller cell buys columns instead of
     costing them. */
  /* Sideways, three columns would be 270px each and the grid would be one row
     tall. Width is the plentiful thing here and height is the scarce one, so
     the count goes back to whatever fits. */
  @media (max-height: 560px) {
    .picker.open { top: max(6px, env(safe-area-inset-top)); gap: 5px; }
    .picker.open .picker-chips { display: none; }
    .picker.open .picker-grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
    .picker.open .pick img { height: 96px; }
    /* Landscape with a keyboard in it leaves the grid about 75px, and 75px is
       either one row of small cells or no row at all. So the hint line goes and
       the cells shrink: the screen is still 844px wide, so a 72px cell is ten of
       them to choose from rather than none. */
    body.kb-up .picker.open .picker-note { display: none; }
    body.kb-up .picker.open .picker-grid { grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); }
    body.kb-up .picker.open .pick img { height: 72px; }
  }

  /* Typing. The mood chips are a shortcut to a word you are in the middle of
     typing anyway, and they cost 41px of a grid that has 197px left. room.js
     writes the class from the same measurement it writes `--kb` from. */
  body.kb-up .picker.open .picker-chips { display: none; }

  /* The handle, and the target for throwing the sheet back down. */
  .picker.open .picker-head::before {
    content: "";
    position: absolute;
    top: -4px;
    left: 50%;
    transform: translateX(-50%);
    width: 40px;
    height: 4px;
    border-radius: 999px;
    background: var(--line);
  }

  .picker.open .picker-head {
    position: relative;
    padding-top: 8px;
  }

  /* The tile would be behind the sheet anyway; drawing half of it poking out of
     the top is worse than not drawing it. It comes straight back on close.

     Not while he is streaming from this phone: that tile is his monitor, and
     `display: none` on a playing <video> is a thing iOS is entitled to pause.
     His is covered by the sheet instead, which costs nothing and touches
     nothing. */
  body.picking.vid-mini:not(.host-live) .stage { display: none; }

  .picker-x { width: 38px; height: 38px; }

  /* Three across, and each one whole.
     *
     * 78px cells were four columns of postage stamp cropped to their top third,
     * which is a grid you cannot choose from. `auto-fill` was still deciding the
     * column count off a minimum width, so the answer moved with the screen;
     * three is what was asked for and three is what a 390px phone should show.
     * The height of each cell comes from the picture itself (see `.pick` in
     * app.css), so the grid is ragged down the right and nothing is cut off. */
  .picker-grid {
    /* `minmax(0, 1fr)`, never a bare `1fr`. A bare one is `minmax(auto, 1fr)`,
       and `auto` there is the item's automatic minimum size, which for a cell
       holding a 600px-wide gif is 600px. Three of those is a grid that scrolls
       sideways off the phone. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    /* The sheet decides the height now. A 220px floor inside a landscape sheet
       pushes the note off the bottom. */
    min-height: 0;
  }

  /* Three columns of a 390px phone is 114px each, so a 116px box is square
     enough that a portrait gif and a landscape one are both comfortable in it.
     The height is a number rather than a ratio on purpose: see `.pick` in
     app.css for what iOS does with the ratio version. */
  .pick img { height: 116px; }
  .pick .fav { width: 32px; height: 32px; font-size: 16px; }
  .picker-tabs button { padding: 9px 13px; }
  .picker-chips button { padding: 7px 11px; }
  .picker-note { font-size: 12.5px; }

  /* One row for both sets of emoji rather than two.

     The reaction bar used to live on the video, which is now a 124px tile that
     cannot hold seven buttons, and the quick-send row is a second row of very
     similar looking circles. They are not the same thing — one fires an effect
     on both screens, the other says a word — so neither could be dropped, but
     they can share a line and scroll. */
  .thumb-row {
    display: flex;
    align-items: center;
    gap: 7px;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }

  .thumb-row::-webkit-scrollbar { display: none; }

  .thumb-row .love-bar.in-chat {
    display: flex;
    flex: 0 0 auto;
    padding: 0;
    gap: 6px;
    background: transparent;
    overflow: visible;
  }

  .thumb-row .quick {
    flex: 0 0 auto;
    flex-wrap: nowrap;
    gap: 6px;
  }

  .thumb-row .quick button {
    flex: 0 0 auto;
    padding: 7px 11px;
    font-size: 17px;
  }

  /* Which of the two emoji rows is real depends on where the video is: when it
     is full width it carries its own reaction bar, and two of them on one
     screen is a bug you cannot explain to anyone. */
  body.vid-full .thumb-row .love-bar.in-chat { display: none; }
  body.vid-full .thumb-row .divider { display: none; }

  .thumb-row .divider {
    flex: 0 0 auto;
    width: 1px;
    height: 22px;
    background: var(--line);
  }

  /* Landscape, where the composer is most of the screen. The emoji row is the
     first thing that can go; everything in it is reachable another way. */
  @media (max-height: 480px) {
    .thumb-row { display: none; }
  }

  /* Set on the wrapper, not the textarea: the hidden copy that sizes the box
     reads the same three numbers, and the two have to agree exactly. */
  .say-wrap {
    --say-min: 44px;
    --say-pad-y: 12px;
    --say-pad-x: 15px;
  }

  /* Four round buttons and a text box on 390px of phone. At 44px with an 8px
     gap the box was 166px, about twenty characters, and the fourth button (the
     camera) is what took it there. 40px with a 6px gap gives 196px back and is
     the same target the header icons already use here. */
  .icon-btn { width: 40px; height: 40px; }
  .composer-row { gap: 6px; }

  /* --------------------------------------------------------- the sheets */

  /* A centred modal is a desktop idea. On a phone the card comes up from the
     bottom edge where the thumb already is, keeps its rounded top corners
     above the fold, and can be thrown back down — phone.js does the throwing;
     this is the shape and the handle that says it can be thrown. */
  .sheet {
    place-content: end stretch;
    padding: 0;
  }

  .sheet-card {
    width: 100%;
    max-height: 88dvh;
    border-radius: 26px 26px 0 0;
    padding: 10px 16px 16px;
    padding-bottom: max(16px, env(safe-area-inset-bottom));
    animation: sheet-up 0.3s cubic-bezier(0.2, 0.9, 0.3, 1);
    /* Written by the drag handler while a throw is in progress. */
    transform: translateY(var(--sheet-y, 0));
    overscroll-behavior: contain;
  }

  .sheet-card.settling {
    transition: transform 0.22s cubic-bezier(0.3, 0.9, 0.3, 1);
  }

  @keyframes sheet-up {
    from { transform: translateY(24px); opacity: 0.6; }
    to { transform: translateY(0); opacity: 1; }
  }

  /* The grab bar. Deliberately part of the card rather than a separate row, so
     the drag target is the whole top of the sheet and not a 4px line. */
  .sheet-grab {
    display: block;
    width: 40px;
    height: 4px;
    margin: 2px auto 12px;
    border-radius: 999px;
    background: var(--line);
  }

  .sheet-card h2 { font-size: 19px; }

  /* Anything you set a value with. 32px targets are a miss on a phone and the
     scale is the control she reaches for most. */
  .dial-quick button { padding: 10px 15px; font-size: 13.5px; }
  .dial-range { height: 34px; }
  .medals-btn, .save-btn, .file-btn { padding: 12px 20px; font-size: 13.5px; }
  .set-actions { gap: 10px; }
  .set-actions .medals-btn, .set-actions .save-btn { flex: 1 1 auto; text-align: center; }
  .us-btn { padding: 14px 8px; }
  .medal-grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
  .chip-row button { width: 46px; height: 46px; }

  /* ---------------------------------------------------------- the search */

  /* Already full-panel from app.css; this is only the target sizes and the
     inset, since searching is the one mode with nothing else on screen. */
  .search-bar { padding: 10px 10px; padding-top: max(10px, env(safe-area-inset-top)); }
  .search-input { padding: 12px 15px; }
  .search-x { width: 40px; height: 40px; }
  .hit { padding: 12px 12px; }

  /* --------------------------------------------------------- the basement */

  /* The bar is fixed to the top of the window and the header now runs under the
     notch, so it has to clear the same inset or it lands on the status bar. */
  body.in-basement .app { padding-top: calc(60px + env(safe-area-inset-top)); }
  body.in-basement .basement .bs-card { top: max(8px, env(safe-area-inset-top)); }
  body.in-basement .toasts { top: calc(88px + env(safe-area-inset-top)); }

  /* The tile is not a fixed element inside .app (it is absolute), so the
     basement's filter still dims it along with everything else. It does need to
     stay under the bars, though. */
  body.in-basement.vid-mini .stage { z-index: 8; }
}

/* Reduced motion: the tile still moves between corners, it just does not
   animate there, and the sheets appear rather than rise. */
@media (max-width: 700px) and (prefers-reduced-motion: reduce) {
  body.vid-mini .stage { transition: none; }
  .sheet-card { animation: none; }
  .sheet-card.settling { transition: none; }
}
