/* zachvalenti.com — one sheet, no preprocessor.
 *
 * Theming works through CSS custom properties: :root sets the default
 * palette, and a data-theme attribute on <html> (toggled by app.js)
 * swaps in another. Every rule below refers to colors as var(--name),
 * so a theme change is one attribute write.
 */

/* reset: browsers ship default margins/paddings; start from zero.
   border-box makes width include padding, which keeps sizing sane */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg:   #0a0c0a;   /* the room */
  --ink:  #9be9a8;   /* body text */
  --dim:  #3d5c46;   /* quiet chrome: counts, buttons, borders */
  --hit:  #4ade80;   /* a line that meets its target */
  --over: #d98a62;   /* a line that runs past it */
  --flag: #e05252;   /* a word from outside the thousand */
  --tide: #58a6c8;   /* the clear-hold water */
  --sel:  rgba(74, 222, 128, .25);
  --fs:   clamp(17px, 2.4vw, 21px); /* fluid type with a floor and ceiling */
  /* How far the toolbar stands off the bottom of the screen, and where
     the fade behind it starts from — one value, because the two have to
     agree or the bar floats in the middle of its own gradient.

     A third of the safe-area inset, not all of it. The whole inset is
     the conservative reading of "keep clear of the home indicator", and
     on a phone it left the count sitting high with nothing underneath
     it, which looks less like margin than like a mistake.

     This is close to the floor, and the floor is worth writing down.
     Measured on a 390x844 phone reporting a 34px bottom inset: the
     buttons' own tap padding puts their glyphs 11px above whatever this
     value is, and the box below them reaches nearly to it. At .35 the
     text sits 32px up and the boxes 21px, which clears the indicator
     itself. Much less and the buttons are inside the strip iOS watches
     for the home gesture, where a first tap gets eaten often enough to
     be infuriating. Space that looks spare here is mostly tap target.

     Zero on anything without an inset, where it comes out at the .5rem. */
  --foot: calc(.5rem + env(safe-area-inset-bottom) * .35);

  /* Where the writing stops and where it is whole again, at each edge —
     the four stops of .room's mask. Hidden as far as the chrome actually
     reaches, then fading in over about as much again, so a line under the
     clock is gone rather than clipped at a rule.

     The foot is here because the bar is on every device. The top is zero
     here and given a height only under (pointer: coarse) below: a desktop
     has nothing across the top of the window to hide from. */
  /* Two colours, two jobs, and keeping them apart is what finally made
     the screen one thing.
     --bg is the page: the paper the writing sits on, what .base paints,
     what the eye reads. It only ever changes with the theme.
     --screen is what iOS shows in the 47px band it owns at the foot. It
     derives that from the document's background colour, so html and body
     carry --screen and nothing else does. At rest the two are identical;
     the flood moves --screen alone, which is why the band can follow the
     tide without the water double-darkening the writing. */
  --screen: var(--bg);
  /* --bg at 45%, the same light the card's ::backdrop leaves. Written out
     per theme rather than mixed: color-mix() is Safari 16.2+, and a custom
     property holding a function the engine cannot parse stays invalid at
     computed-value time, which takes `background: var(--screen)` with it and
     blanks the band outright. A hex cannot fail. */
  --screen-dim: #050605;

  --hide-top: 0px;
  --show-top: 0px;
  --hide-bot: calc(var(--foot) + 1.9rem);
  --show-bot: calc(var(--foot) + 3.4rem);

  color-scheme: dark; /* tells the browser to render UA widgets dark */
}

/* creme paper, black ink */
:root[data-theme="paper"] {
  --bg:   #f6f1e3;
  --ink:  #221d15;
  --dim:  #b3a789;
  --hit:  #2f7d46;
  --over: #c05f33;
  --flag: #c0392b;
  --tide: #3d7ea6;
  --sel:  rgba(47, 125, 70, .22);
  --screen-dim: #6f6c66;
  color-scheme: light;
}

/* dusk: candlelight gold on deep violet */
:root[data-theme="dusk"] {
  --bg:   #171321;
  --ink:  #e6c98f;
  --dim:  #5d5372;
  --hit:  #ffcf6e;
  --over: #e2725b;
  --flag: #ef6f6f;
  --tide: #6ea8d8;
  --sel:  rgba(230, 201, 143, .25);
  --screen-dim: #0a090f;
}

/* The document itself never scrolls. The room inside it does.
 *
 * This is one line of CSS and the answer to a bug that took three goes
 * to understand. A phone's keyboard does not shorten the page it covers:
 * it shrinks the *visual* viewport — what you can see — and leaves the
 * layout viewport, the one 'position: fixed' measures against, exactly
 * as tall as it was. The browser then slides the visual viewport around
 * inside it to keep the caret in sight, and every fixed thing goes with
 * it. That is why the toolbar wandered, and why the faded edges wandered
 * after it: they were pinned to a frame that was itself moving.
 *
 * Nothing can be pinned to a moving frame. So the frame is made to hold
 * still — the document is given no scroll at all, and the writing scrolls
 * inside .room instead. There is then nothing for the browser to slide,
 * and the edges and the bar stay where they are put, whatever the
 * keyboard does and however far the poem runs.
 */
html, body {
  height: 100%;
  overflow: hidden;
}

/* html and body are the channel, not the picture: iOS derives the band it
   owns from the document's background colour, and both of these are hidden
   behind .base inside the viewport, so moving them is invisible to the
   page and visible only in the band. */
html {
  /* The first paint, and the fallback if script never runs. From then on
     paintScreen() mirrors --screen onto this element's own inline
     background-color, which outranks this rule and is the only write iOS
     Safari notices — a colour arriving through var() paints the page and
     tells the browser nothing. See paintScreen() in app.js; the phone
     reading that established it is in the skill. */
  background: var(--screen);
}

/* the page itself — fixed to the viewport, under the writing, over the
   channel, and deliberately outside the mask */
.base {
  position: fixed;
  inset: 0;
  background: var(--bg);
  pointer-events: none;
  /* the palette ease, moved here off body — this is the layer a theme swap
     is actually seen on, and easing it cannot lag the band */
  transition: background-color .25s ease;
}

.room {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  /* iOS standalone: a bottom-edge rubber-band can wedge the scroll */
  overscroll-behavior-y: none;

  /* No background here on purpose. It had one for exactly one release and
     the mask below fades whatever it is given — which laid a visible seam
     across both edges the moment the flood made this colour differ from
     body's. The page is .base now; the mask is for the writing. */

  /* The writing fades out at the two edges the app doesn't own.
   *
   * At the foot sits the toolbar, on every device; across the top of a
   * phone sit the clock, the signal and the battery, and with
   * viewport-fit=cover the writing runs underneath them. Neither is a
   * place a line should be legible.
   *
   * This is a mask on the room rather than a scrim over it, and the
   * difference is the whole point. A scrim is an opaque layer between the
   * writing and everything painted after it — and what is painted after
   * it is the clear-hold water, a wash of about a quarter opacity. Water
   * over writing is water; water over an opaque band is a dead slab, with
   * the wave crest cut off flat where the band starts. That version came
   * out framed, top and foot, and needed the bands driven out of the
   * water's way frame by frame from JavaScript to look right.
   *
   * Masking removes the writing instead of covering it. Nothing opaque is
   * left anywhere, so nothing can occlude anything: the water flows over
   * the whole screen at every height, and the fade costs one declaration
   * and no script at all.
   *
   * The mask is positioned against the room's own box, which is the
   * viewport — the writing scrolls underneath it and it stays put. The
   * toolbar is outside the room and so is never touched by it, which is
   * what keeps it readable while the writing behind it fades. */
  --edge-mask: linear-gradient(to bottom,
    transparent 0,
    transparent var(--hide-top),
    #000 var(--show-top),
    #000 calc(100% - var(--show-bot)),
    transparent calc(100% - var(--hide-bot)));
  -webkit-mask-image: var(--edge-mask);
          mask-image: var(--edge-mask);
}

/* keep the room still while the context card is up — the card's own
   scroll must not chain into the page behind it. :has() lets the page
   react to the state of an element inside it */
html:has(dialog[open]) .room { overflow: hidden; }

/* And the card's ::backdrop cannot reach the band iOS keeps below the
   viewport — a backdrop covers the viewport by definition, which is exactly
   how that band was identified. So the band is dimmed by the one property
   that does reach it, to the same 55% black the backdrop lays down. Without
   this the card opens over a dimmed screen with a bright strip along the
   foot, which reads as broken and was. */
html:has(dialog[open]) { --screen: var(--screen-dim); }

body {
  background: var(--screen);
  color: var(--ink);
  /* ui-monospace = whatever monospace the OS considers native */
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  caret-color: var(--hit);
  -webkit-tap-highlight-color: transparent;
  /* The ink eases on a theme swap. The background deliberately does not:
     body is the channel iOS reads the band below the viewport from, and an
     ease on that colour is a quarter-second of lag on it. floodBand() steps
     the band in ~190ms and the fall lasts 700ms, so easing here left the
     band lit after the water had drained out from under it — a solid bar
     along the foot on release, reported and real. The palette ease belongs
     to what the eye actually sees, which is .base. */
  transition: color .25s ease;
}

/* Held still for the one frame a theme changes on — see applyTheme().
   Half the room can ease its colours and half of it cannot, and a
   gradient is in the half that cannot, so nothing eases and the palette
   lands whole.

   The gradient this was written for is gone: the faded edges are a mask
   now, which is pure alpha and carries no colour to arrive early. So
   this is very likely inert, and could go — but the flash it fixed was
   real and reported, and one frame without transitions costs nothing, so
   it stays until someone has looked at a theme switch on a phone and
   decided otherwise. */
html.swapping, html.swapping * { transition: none !important; }


::selection { background: var(--sel); }

.page {
  max-width: 38rem;
  margin: 0 auto; /* the classic centering move: auto side margins */
  /* of the room, which is the viewport — .page no longer measures itself
     against the document, because the document has no height to speak of */
  min-height: 100%;
  font-size: var(--fs);
  line-height: 2em; /* computes to px so the gutter inherits the exact line grid */
  /* generous top/bottom padding keeps the writing line mid-screen;
     the right side reserves room for the count gutter */
  padding: 22vh calc(1rem + 3.4em) 45vh 1.25rem;
  cursor: text;
}

/* positioning context: the gutter's absolute coordinates and the
   line offsets app.js measures are all relative to this box */
.doc { position: relative; }

.editor {
  outline: none;          /* the whole page is the input; no focus ring */
  white-space: pre-wrap;  /* honor typed spaces and line breaks */
  overflow-wrap: break-word;
  min-height: 2em;
}

/* the count column: absolutely positioned just past the text column,
   inert to clicks and selection — numbers are scenery, not content */
.gutter {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 100%;
  width: 3.4em;
  pointer-events: none;
  user-select: none;
}

/* each count is a span pinned by app.js to its line's offsetTop */
.gutter span {
  position: absolute;
  left: 1.3em;
  font-size: .68em;
  color: var(--dim);
  font-variant-numeric: tabular-nums; /* digits share a width; no jitter */
  transition: color .25s ease;
}

/* app.js labels each count via data-state; colors hang off that */
.gutter span[data-state="hit"]  { color: var(--hit); }
.gutter span[data-state="over"] { color: var(--over); }

/* Markdown.
 *
 * Both habits at once: Ulysses on the line you're writing — the marks
 * there, dimmed to the same quiet as the chrome, so nothing shifts under
 * the caret — and Obsidian everywhere else, where they step out of the
 * way and the page reads as the poem rather than as its notation.
 *
 * The marks that go are the ones that decorate words, plus the hashes of
 * a heading, which its size announces well enough on its own. The ones
 * that stay are a line's own shape: a list stripped of its bullet reads
 * as broken, not tidy. Either way the characters are only hidden, never
 * removed — they are still in the file and still in anything you copy. */
.md-mark, .md-hash, .md-prefix { color: var(--dim); }
.md-strong { font-weight: 700; }
.md-em     { font-style: italic; }
.md-u      { text-decoration: underline; text-underline-offset: .2em; }

.editor > :not(.open) .md-mark,
.editor > :not(.open) .md-url,
.editor > :not(.open) .md-hash { display: none; }

/* a word the thousand doesn't hold */
.md-odd { color: var(--flag); }

/* what the first line asked for: present, quiet, unexplained */
.editor > .md-mode { color: var(--dim); }

/* a word the page answers to: chrome, not ink — see flagCommand() */
.md-cmd { color: var(--dim); }

/* Headings scale up but have to land back on the same 2em line grid the
   gutter numbers are pinned to. A line-height in em would resolve
   against the heading's own larger font, so these are unitless ratios of
   base ÷ heading size — 2 / 1.4, 2 / 1.2, 2 / 1.08. h4–h6 keep the body
   size and separate themselves by weight alone. */
.editor > .md-h1 { font-size: 1.4em;  line-height: 1.43; font-weight: 700; }
.editor > .md-h2 { font-size: 1.2em;  line-height: 1.67; font-weight: 700; }
.editor > .md-h3 { font-size: 1.08em; line-height: 1.85; font-weight: 700; }
.editor > .md-h4,
.editor > .md-h5,
.editor > .md-h6 { font-weight: 700; }

/* quoted lines step back in color rather than growing a left border —
   a border would shift the text column and knock the poem out of line
   with itself */
.editor > .md-quote {
  color: var(--ink); /* fallback where color-mix is unsupported */
  color: color-mix(in srgb, var(--ink) 72%, var(--dim));
}

/* Fountain — /fountain on the first line, and the room is a screenplay.
 *
 * Every rule here keeps the base font size on purpose. The gutter is
 * hidden in this mode, so the 2em line grid the headings above go to
 * such trouble to land back on doesn't strictly matter — but a script
 * is read as a column of even lines, and growing the sluglines would
 * cost more than it bought. Shape comes from weight, indent and
 * alignment instead.
 *
 * Nothing here uses text-transform. A cue is shouted because the writer
 * shouted it, and the app's standing promise is that the file holds
 * exactly what the page shows. */
.editor > .ftn-scene   { font-weight: 700; }
.editor > .ftn-section { font-weight: 700; }

/* Sluglines and transitions the app worked out for itself are shouted
   for you, so "int. kitchen" can be typed in whichever case is nearest
   to hand. This is the one place the app shows something other than
   what was typed — and it isn't only cosmetic: the export shouts the
   same lines (ftnText), because a lowercase "cut to:" isn't a
   transition to any other Fountain program. A forced heading or
   transition keeps whatever case you gave it; forcing is the writer
   overruling the app, and overruling that back would be a poor trade.
   A forced cue — @maya — is the exception, and fountain.js says at
   length what that buys and what it spends. */
.editor > .ftn-caps { text-transform: uppercase; }

/* dialogue steps in from the margin the way it does on the page, and
   the cue steps in further. padding rather than margin, so the line
   keeps its full width for the caret to sit in */
.editor > .ftn-character { padding-left: 8em; }
.editor > .ftn-paren     { padding-left: 6em; }
.editor > .ftn-dialogue  { padding-left: 4em; padding-right: 4em; }

.editor > .ftn-transition { text-align: right; }
.editor > .ftn-centered   { text-align: center; }

/* an aside is quieter than the script; a parenthetical is an aside the
   actor still reads, so it stops short of the boneyard's dimness */
.editor > .ftn-paren {
  color: var(--ink); /* fallback where color-mix is unsupported */
  color: color-mix(in srgb, var(--ink) 72%, var(--dim));
}

/* A dual speech sits alongside the one before it rather than under it.
   Two real columns aren't available in a single flow of lines, so the
   second speech is pushed right instead — enough to read as "at the same
   time" without pretending to be a rendered page.

   Set as a share of the column rather than in em, so the second speech
   keeps a usable width to wrap into. Fixed indents deep enough to read
   as "alongside" leave a two-character ribbon on a 38rem page. */
.editor > .ftn-character.ftn-dual { padding-left: 55%; }
.editor > .ftn-paren.ftn-dual     { padding-left: 52%; }
.editor > .ftn-dialogue.ftn-dual  { padding-left: 48%; padding-right: 0; }

/* The title page. Dim like the other scaffolding, because it is the
   file's business rather than the script's. */
.editor > .ftn-title { color: var(--dim); }

/* The suggested rest of a character's name. Painted, never typed: this
   is a pseudo-element, so it is not a child node, and nothing that reads
   the line — getText(), currentRuns(), the caret — can see it at all.
   It stays inert to the pointer too: a tap lands on the line div either
   way, and app.js works out from the line's own geometry whether it
   landed where this is drawn. */
.editor > [data-ghost]::after {
  content: attr(data-ghost);
  color: var(--dim);
  pointer-events: none;
}

/* The /version readout, painted on the mode line that asked for it. Same
   pseudo-element as the suggestion above and for the same reasons — see
   the note over setRelease() in app.js. Dim, because a mode line is
   scaffolding and this is a reading off the scaffolding. */
.editor > .md-mode[data-release]::after {
  content: attr(data-release);
  color: var(--dim);
  pointer-events: none;
  /* the readout carries newlines now — it reports the water's own geometry
     as well as the release, because probe.html cannot answer for an
     installed app and this is the only instrument that runs inside one */
  white-space: pre-wrap;
}

.editor > .ftn-lyric    { font-style: italic; }
.editor > .ftn-synopsis { color: var(--dim); font-style: italic; }

/* A link: the words underlined, the address dim beside them until the
   line is left alone, at which point it goes entirely (see the rule
   above) and the words stand on their own.

   The underline is offset and thin so it reads as a link rather than as
   __underline__, which this app already spells a different way. The
   pointer only appears on a line you aren't writing, because that is the
   only time a click follows the link — see the pointerdown handler in
   app.js. Making the cursor tell the truth about that is most of what
   teaches it. */
.editor > :not(.open) .md-link {
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: var(--dim);
}
.editor > :not(.open) .md-link:hover { text-decoration-color: var(--hit); }
.md-url { color: var(--dim); }

/* the boneyard: kept in the file, struck from the script */
.editor > .ftn-bone { color: var(--dim); text-decoration: line-through; }

/* a note to yourself, dim from bracket to bracket */
.md-note { color: var(--dim); }

/* The one hint the page gives, shown only while it is empty.
 *
 * A card with a hidden menu behind it has a problem a card doesn't: a
 * visitor who clears the page is left staring at nothing, with no way
 * to know there was ever anything else. So an empty page says the one
 * word that leads to all the others, and says it in the chrome colour,
 * quietly, the way a hint should. */
.doc.empty::after {
  content: "/menu";
  position: absolute;
  top: 0;
  left: 0;
  color: var(--dim);
  pointer-events: none;
}

/* the bottom toolbar spans the viewport; env(safe-area-inset-*) pads
   past notches and home-bar indicators on phones */
.bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: .5rem calc(.8rem + env(safe-area-inset-right))
           var(--foot)
           calc(.9rem + env(safe-area-inset-left));
  /* the bar itself ignores clicks so the page stays writable beneath
     it; the .tools groups opt back in */
  pointer-events: none;
  font-size: .8rem;
  color: var(--dim);
  letter-spacing: .05em;
}

.tools {
  display: flex;
  align-items: center;
  gap: .1rem;
  pointer-events: auto;
}

/* the count sits centered in the open water between ? and the tools.
   It's secretly a button (click cycles syllables → words → timer), so
   it dresses as plain text and only brightens on hover */
.bar > .total {
  flex: 1;
  text-align: center;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  pointer-events: auto; /* the bar ignores clicks; opt back in */
  padding: .2rem 0;
  font-variant-numeric: tabular-nums; /* the timer ticks without wobble */
  transition: color .25s ease;
}

.bar > .total:hover { color: var(--ink); }

/* buttons dressed down to quiet text glyphs */
.tool {
  background: none;
  border: 0;
  color: var(--dim);
  font: inherit;
  font-size: .935rem;
  cursor: pointer;
  opacity: .6;
  padding: .2rem .45rem;
  transition: color .25s ease;
}

.tool:hover { color: var(--ink); opacity: 1; }

/* the hold-to-clear button must own its gesture: no text selection,
   no touch scrolling, no iOS callout menu mid-hold */
.clear {
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.clear.holding { color: var(--tide); opacity: 1; }

/* hold progress: an ocean tide takes the full three seconds to reach the
   top of the page; let go early and it falls away. The water itself —
   three translucent layers whose crests deform as they drift — is drawn
   on this canvas by app.js */
/* The clear-hold water, and it has to reach the real bottom of the screen.
 *
 * This said `inset: 0; width: 100%; height: 100%` and that is not what it
 * looks like. Give an absolutely positioned box `top`, `bottom` *and* a
 * height and it is over-constrained, so the used value of `bottom` is
 * thrown away (CSS 2.1 §10.6.4; `right` goes the same way in §10.3.7).
 * The canvas was therefore never pinned to the bottom edge at all — it
 * hung from the top and took its size from a percentage.
 *
 * Which is fine until the percentage and the screen disagree, and in an
 * installed iOS app they do: 100% of the fixed containing block leaves
 * out the home-indicator strip. The page still painted that strip, in
 * `--bg`, because the root background covers the whole viewport canvas
 * whatever the elements do — so the tide rose over everything, stopped
 * short, and left a dry band of theme colour along the very bottom with
 * the flooded page directly above it.
 *
 * So: hang from the top as it really did, and add the strip back
 * explicitly. env() is `0` on any device without an inset, which makes
 * this identical to the old rule everywhere the old rule was right. The
 * bitmap follows on its own — fit() measures this box, not the window.
 */
/* The water. Plain 100% now: paint is clipped to the viewport whatever
   this says, so the env() sums and max() overdraws that v54-v59 piled on
   here were buying nothing. The band past the viewport is --screen's. */
.flood {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* the ? is the only way into the card; let it sit a little plainer and
   a little larger than the chrome it shares a bar with */
#context { font-size: 1.1rem; opacity: .85; }

.fs { font-size: 1.155rem; }

/* the context card: same quiet room, one step removed */
.about {
  margin: auto; /* the * reset kills the UA centering; restore it */
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--dim);
  border-radius: 4px;
  max-width: min(32rem, calc(100vw - 2.5rem));
  max-height: min(78vh, 100vh - 3rem);
  overflow-y: auto;
  overscroll-behavior: contain; /* card scroll stays in the card */
  padding: 1.5rem 1.7rem;
  font-size: .85rem;
  line-height: 1.75;
  letter-spacing: .01em;
}

/* ::backdrop is the layer a modal <dialog> puts over the page */
.about::backdrop { background: rgba(0, 0, 0, .55); }

/* space paragraphs only where one follows another */
.about p + p, .about blockquote { margin-top: 1em; }

/* a full blank line before each section header */
.about .section { margin-top: 2em; }

/* the card is already monospace; the marks it quotes just need to sit
   apart from the prose around them */
.about code { color: var(--hit); }

.about .form {
  color: var(--hit);
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

/* the sheet's only links live in this card; tint them like a met target */
.about a {
  color: var(--hit);
  text-decoration-color: var(--dim);
  text-underline-offset: .2em;
}

.about blockquote {
  color: var(--ink); /* fallback where color-mix is unsupported */
  color: color-mix(in srgb, var(--ink) 72%, var(--dim));
  border-left: 2px solid var(--dim);
  padding-left: 1em;
}

.about-close {
  position: absolute;
  top: .4rem;
  right: .5rem;
  font-size: 1.1rem;
  line-height: 1;
}

.about .go {
  display: block;
  margin: 1.6em auto 0;
  background: none;
  border: 1px solid var(--dim);
  border-radius: 3px;
  color: var(--ink);
  font: inherit;
  padding: .55em 1.5em;
  cursor: pointer;
  letter-spacing: .06em;
  transition: color .25s ease, border-color .25s ease;
}

.about .go:hover { color: var(--hit); border-color: var(--hit); }

/* touch screens: same quiet glyphs, thumb-sized targets.
   (pointer: coarse) matches fingers rather than screen size */
@media (pointer: coarse) {
  .tools { gap: .4rem; }
  .tool { padding: .75rem .6rem; }

  /* The other edge, which only a phone has: the clock, the signal bars
     and the battery sit across the top of the screen, and with
     viewport-fit=cover the writing runs underneath them. Giving the top
     two stops a height is the whole of it — the mask on .room is already
     written, and on a desktop these are zero and it simply has no top
     fade to draw.

     --view-top is what a keyboard costs. app.js sets it when iOS slides
     the visible box up to keep the caret above the keys; without it the
     top of the mask goes up off the screen with everything else laid out
     against the layout viewport, and the writing runs clear under the
     clock again. Only the top stops take it. The foot does not: down
     there the keyboard *is* the edge, and a fade lifted above the keys
     lays a washed-out stripe across the middle of the writing, which is
     worse than the problem it solves. */
  :root {
    --hide-top: calc(env(safe-area-inset-top) + 1rem + var(--view-top, 0px));
    --show-top: calc(env(safe-area-inset-top) + 3.4rem + var(--view-top, 0px));
  }

  /* There is no Tab key here, so the suggestion has to look like something
     a thumb can take: a faint chip of the color the page already uses
     behind text. Every property is paint — horizontal padding on this
     inline box would shove the ghost off the end of the word it continues
     and could pull it onto the next row, where a spread shadow costs the
     2em line grid nothing at all.

     The letters lift a little off --dim, because a chip behind them takes
     back exactly the contrast they had, and a suggestion you can't read
     isn't one. Where color-mix is unsupported the declaration is dropped
     and the rule above stands, which is the quiet version of the same. */
  .editor > [data-ghost]::after {
    color: color-mix(in srgb, var(--ink) 45%, var(--dim));
    background: var(--sel);
    box-shadow: 0 0 0 .15em var(--sel);
    border-radius: .25em;
  }
}

/* installed as an app there's no browser chrome to escape from —
   drop the fullscreen button */
@media (display-mode: standalone), (display-mode: fullscreen) {
  .fs { display: none; }
}
