/*
  schwarze.cloud -- interface
  ===========================
  One light range, deliberately: there is no dark mode and no second set of
  values anywhere in this file. Ink on these greys is for text and for the odd
  deliberate block, not for a whole second world.

  Everything measurable lives in the token block below -- surfaces, ink,
  spacing, type, icon sizes, the smallest thing a finger can hit. Redesigning
  then means changing values rather than hunting through rules, and a value
  that is not in the block is a value that drifted.
*/

/* ---------- type ----------
   Served from here rather than from a content delivery network: a service
   worker can only cache its own origin, which the planned installable app
   needs, and nobody outside gets to see who uses this installation.
   `font-display: swap` shows the system face at once and exchanges it when
   Plex arrives; empty text on a slow mobile connection is the worse of the
   two evils. */

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-latin-500-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-latin-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2");
}

/* ---------- tokens ---------- */

:root {
  color-scheme: light;

  /* Surfaces. Back to a light ground, which is where the air is.

     The grey shell that was here had one cause: a white glow can only fall on
     something dark enough to show it, so the soft light forced a mid grey
     underneath it and every surface then had to crowd into twenty-two of two
     hundred and fifty-six steps. You can have the moulded key or you can have
     the white space; that technique does not allow both.

     What it cost was not the surfaces but the text -- ink went from 13.8:1 to
     10.0:1 and the eleven pixel meta line from 5.8 to 4.2. On a light ground
     every piece of type on the page gains roughly a third of its contrast
     back, which is the whole of the answer to "not contrast-rich enough". */
  --ground: #f2f1ed;
  --raised: #ffffff;   /* a button, a panel, the row menu */
  --hover: #e9e7e2;
  --chrome: #e6e4df;   /* a strip that opens out of the page */

  /* The keys, as their own ladder rather than borrowed from the surfaces.
     They were white, grey and ink; the grey one was the only one anybody
     liked, and the reason is arithmetic: a key is lit from above, and a white
     highlight on a white face has nowhere to fall. Measured in lightness
     steps, the light edge was worth +8.3 on the grey face and exactly +0.0 on
     the white one.

     So every face moves down far enough to carry an edge. White becomes a
     light grey, the grey becomes darker, and the ink lifts -- the last so its
     own edges have somewhere to sit instead of being swallowed. The ground
     does not move: it is the air, and the keys are what sits on it.

     The ladder runs the other way from a first guess: the less a key matters,
     the lighter it is. Adding a folder and choosing files act on what is in
     the folder, so they carry the weight; the view only changes how the same
     thing is looked at, and steps back by standing closest to the page. Ink is
     the far end of the same line, not a fourth idea. */
  --key: #d8d4c9;        /* acts on the folder: new folder, choose */
  --key-quiet: #e7e4dc;  /* only changes the looking: the view */
  --key-ink: #35353e;    /* the default action */
  /* The floor of the player's groove: 8.4 lightness steps below a key's face,
     which is the depth this interface reads as "below the surface".

     It was the fill of a pressed key too, and a key no longer changes colour
     when it is pressed -- the light moves instead. Two statements of the same
     thing made a row of keys restless: whichever was engaged read as a
     different button rather than as the same button held down. */
  --key-down: #c2bdae;

  /* The floor of a groove. A row pressed into the strip carried the strip's
     own colour and two hairlines, and the hairlines were the whole of the
     depth -- which at a glance was not enough to say which of four things was
     the one in force. Just under four steps of lightness below the strip
     (90.6 to 86.7), which is where a difference is read without being
     announced: the same distance the keys use between a face and its own
     pressed tone, halved, because this is a row and not a key. */
  --sunk: #dcd9d2;

  /* The one dark world: the ground a photograph is looked at against, and the
     wash laid over a photograph so something can be read on top of it.

     These were dropped out of this block when the surfaces went grey, and five
     rules in the viewer went on asking for them -- so the backdrop behind a
     photograph resolved to nothing and was transparent. Rewriting a token
     block without sweeping its consumers is exactly the mistake this file has
     a habit of catching everywhere else. */
  --dark: #14141a;
  --veil: rgb(0 0 0 / 0.45);

  /* Two edges rather than one. A refined three dimensional thing is lit from
     above, so its lower edge is the one that catches shade -- one line a
     little darker along the bottom is the whole of the roundness. */
  --edge: #d8d4ca;
  --edge-low: #bdb8ab;
  --line: #b0aea8;
  --line-soft: #d9d5cb;

  /* Ink. Not pure black: on these greys a trace of grey takes the hardness off
     the edges without costing contrast. Every value here was measured against
     the surface it sits on, and the faintest still clears 4.5:1 for text at
     the smallest size in use. */
  --ink: #23232a;
  --ink-muted: #4e4d55;
  --ink-faint: #5d5c65;

  /* The six stripes, in their order from the 1977 mark. They carry exactly two
     meanings: something is happening, and this is us. Spent on buttons, links
     or states as well they would stop meaning either. */
  --r1: #61bb46;
  --r2: #fdb827;
  --r3: #f5821f;
  --r4: #e03a3e;
  --r5: #963d97;
  --r6: #009ddc;

  /* Hard bands, no blending: the point of the stripes is that you can count
     them. */
  --rainbow: linear-gradient(
    90deg,
    var(--r1) 0 16.667%, var(--r2) 16.667% 33.333%, var(--r3) 33.333% 50%,
    var(--r4) 50% 66.667%, var(--r5) 66.667% 83.333%, var(--r6) 83.333% 100%);

  /* The work an accent usually does is done by ink and by weight. Only the
     focus ring takes a colour, and it takes the blue from the set rather than
     a seventh one from nowhere. */
  --focus: #0071a4;
  --danger: #b02a2a;

  /* Spacing, on a four pixel grid. Six steps is enough for an interface this
     size; the values in between that accumulated before -- 0.35, 0.45, 0.55,
     0.65 -- were decisions nobody made. */
  --s1: 0.25rem;  /*  4px */
  --s2: 0.5rem;   /*  8px */
  --s3: 0.75rem;  /* 12px */
  --s4: 1rem;     /* 16px */
  --s5: 1.5rem;   /* 24px */
  --s6: 2rem;     /* 32px */

  /* Type. Four sizes: the meta line, secondary text, everything normal, and
     the wordmark. */
  --t-xs: 0.7rem;   /* 11px -- meta lines, measured at 5.8:1 on the ground */
  --t-sm: 0.8rem;   /* 13px -- status, quota, secondary labels */
  --t-md: 0.94rem;  /* 15px -- names, buttons, body */
  --t-ml: 1.1rem;   /* 18px -- the wordmark in the side column */
  --t-lg: 1.35rem;  /* 22px -- the wordmark on the sign-in screen */

  /* The icon that stands on its own in a control, tied to the 44 pixel control
     it sits in: at 18 it used less than half of it, and the 44 is about where a
     finger lands rather than about the glyph.

     The smaller one that sits inside a line of text has no token here. Every
     icon in this interface is drawn by the script, which passes a number, and
     only this size is ever needed in a rule as well -- a token nothing reads
     is a decision that has quietly stopped applying. See ICON_SM in app.js. */
  --i-md: 20px;

  /* The smallest a finger hits reliably. Every control is at least this in
     both directions -- which is the whole point of a mobile-first interface
     and was true of none of them before this pass. */
  --tap: 44px;

  /* The column beside the content, and it is a telephone wide -- 390 CSS
     pixels, the width every decision in the strip was made at. The view
     choices in two columns, the QR code at 168, the player bar with five
     controls and a slider, the way the sharing bands wrap: all of it holds
     there without being measured again. A column of any other width would
     mean deciding each of those a second time. */
  --rail: 24.375rem;

  /* Eight. The keys earned it and the rest of the interface follows them
     rather than the other way round. */
  --radius: 8px;
  --radius-sm: 6px;

  /* The light. Flat, and the depth comes from hovering rather than from being
     moulded: a hairline of light along the top edge, a contact shadow tight
     under the object, and a second softer one for the height it stands at.

     No hard offset, no skirt, no glow. The first was a drawn shadow and read
     as graphic rather than as an object; the second needed a grey ground; and
     both of them were louder than a thing somebody looks at every day should
     be. The retro moved into the identity instead -- the stripes, the type,
     the cloud, the language. */
  --up:
    inset 0 1px 0 rgb(255 255 255 / 0.52),
    inset 0 -1px 0 rgb(0 0 0 / 0.06),
    0 1px 1px rgb(0 0 0 / 0.05),
    0 2px 6px rgb(0 0 0 / 0.07);
  --down: inset 0 1px 2px rgb(0 0 0 / 0.1);
  /* The same two edges on the lesser key, at the alpha that gives them the
     same weight there. A darker face takes more of a white edge from less
     paint, so one set of numbers across three faces would make one of them
     shout -- the step is what should match, not the ingredient. */
  --up-quiet:
    inset 0 1px 0 rgb(255 255 255 / 0.85),
    inset 0 -1px 0 rgb(0 0 0 / 0.06),
    0 1px 1px rgb(0 0 0 / 0.05),
    0 2px 6px rgb(0 0 0 / 0.07);
  /* Ink carries both a little harder. A dark face swallows an edge at arm's
     length, and this is the one key that has to be found without looking. */
  --up-dark:
    inset 0 1px 0 rgb(255 255 255 / 0.16),
    inset 0 -1px 0 rgb(0 0 0 / 0.3),
    0 1px 1px rgb(0 0 0 / 0.12),
    0 2px 8px rgb(0 0 0 / 0.18);
  --down-dark: inset 0 1px 3px rgb(0 0 0 / 0.5);
  /* A one pixel recess, for a row inside a strip rather than a key on the
     page. It is the key's two edges the other way round -- shade along the top,
     light along the bottom -- and no fill at all, so the row stays the colour
     of the strip it is pressed into. */
  --down-soft:
    inset 0 1px 0 rgb(0 0 0 / 0.086),
    inset 0 -1px 0 rgb(255 255 255 / 0.54);

  /* A panel lying over the page, and the shadow the wordmark's cloud throws.
     The second is a filter rather than a box-shadow, so it follows the
     silhouette instead of the square the cloud is drawn in. */
  --float: 0 6px 24px rgb(0 0 0 / 0.14), 0 1px 3px rgb(0 0 0 / 0.1);
  --cast: 0 2px 3px rgb(0 0 0 / 0.16);
}

* {
  box-sizing: border-box;
  /* No blue-grey flash on tap. The interface says what it thinks about being
     pressed -- the lift goes down, a row takes the hover fill -- and the
     browser's own rectangle is a second answer in a colour from nowhere. */
  -webkit-tap-highlight-color: transparent;
}

/* Long pressing a row opens our menu, so the two things a browser does with a
   long press have to stop: selecting the text under the finger, and, on iOS,
   opening its own sheet of link actions over the top of ours. */
.row,
.tile {
  user-select: none;
  -webkit-touch-callout: none;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: var(--t-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* The loading phase is bridged by the rule alone: it is already sweeping when
   the page arrives, and a line of text under it only adds something to read
   and then take away again. The words stay for anyone who cannot see a line
   sweep, and only for them. */
.booting {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Figures in a column need digits of equal width, or sizes and progress
   readings jitter every time they update. */
.tabular { font-variant-numeric: tabular-nums; }

/* ---------- controls ---------- */

/* Every button clears the tap size in both directions. Where that is more room
   than the design wants to show, the padding does the growing and the visible
   weight stays where it was -- a control can be quiet and still be catchable.

   Every button that is a surface also lifts. The shadow says "this is a thing
   you can press", which is true of all of them equally; how much a press
   matters is said by the fill, and saying it twice would leave the secondary
   button looking like it does less than it does. */
button {
  font: inherit;
  font-size: var(--t-md);
  color: var(--ink);
  background: var(--key);
  border: none;
  border-radius: var(--radius);
  min-height: var(--tap);
  padding: var(--s2) var(--s3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  cursor: pointer;
  border: 1px solid var(--edge-low);
  /* Lit from above, so the lower edge is the one in shade. One line a shade
     darker along the bottom is the whole of the roundness -- the cheapest
     three dimensions there are, and the quietest. */
  border-bottom-color: var(--line);
  box-shadow: var(--up);
}
/* Under the pointer it stands a little higher rather than changing colour.
   Height is what a raised thing has; a second fill would be a second idea. */
button:hover { border-color: var(--edge-low); box-shadow: var(--up), 0 3px 10px rgb(0 0 0 / 0.06); }
/* Pressed is a change of light, not a change of colour.

   The fill stays exactly what it was and the shadow turns over: the highlight
   along the top edge becomes a shade inside it, and the key drops the pixel it
   was standing on. A key that also darkens is saying the same thing twice, and
   the second saying is the one that makes a row of them restless -- whichever
   is engaged reads as a different button rather than as the same button held
   down. */
button:active {
  border-color: var(--edge-low);
  box-shadow: var(--down);
  transform: translateY(1px);
}

/* Held down rather than merely pressed: the key that is engaged stays engaged.
   A latching key is the one thing on a machine like this that says "this mode
   is on" without a word, and it is the same gesture the finger just made, left
   where it was. Used for the place you are standing in and for a control whose
   strip is open underneath it.

   Everything that opted out of the lift by saying box-shadow: none opted out
   of the recess in the same breath -- the menu rows, the folder rows, the quiet
   buttons. Those are text with a hit area, and a hit area is not a key. */
button.down {
  border-color: var(--edge-low);
  box-shadow: var(--down);
  transform: translateY(1px);
}

/* Ink. Weight rather than colour marks the default action -- the six stripes
   are spoken for elsewhere, and a seventh hue would dilute them. */
button.primary {
  background: var(--key-ink);
  border-color: var(--key-ink);
  /* Its own colour, a shade down, rather than black: against a face this light
     a black line is a drawn outline, and the point of the lower edge is shade
     on the object, not a border around it. */
  border-bottom-color: var(--ink);
  color: var(--raised);
  font-weight: 500;
  box-shadow: var(--up-dark);
}
button.primary:hover {
  background: var(--ink);
  border-color: var(--ink);
  box-shadow: var(--up-dark), 0 3px 12px rgb(0 0 0 / 0.16);
}
button.primary:active, button.primary.down {
  box-shadow: var(--down-dark);
}

/* The other half of the rule above: a quiet button is not a surface. It has no
   fill and no edge until the pointer is on it, and a shadow under a shape with
   no edges is a smudge rather than a lift. Same for the three that follow --
   the row menu, the items inside a menu, the breadcrumb -- which are all
   text with a hit area around it. */
button.quiet {
  background: none;
  border-color: transparent;
  color: var(--ink-muted);
  box-shadow: none;
}
button.quiet:hover { color: var(--ink); background: var(--hover); border-color: transparent; box-shadow: none; }
button.quiet:active { background: var(--hover); box-shadow: none; transform: none; }
button.quiet.danger:hover { color: var(--danger); }
/* A quiet control with a band open under it.

   These opted out of the cap and the lift, so pressed cannot be shown by
   taking a lift away that was never there. The fill does it instead, and it is
   the same fill the chosen sort key wears: darker than the ground it sits on.

   It matters most here. A quiet button is the least button-looking thing in
   the bar, and it is the one that opens the menu -- so while the menu is open
   it is also the one with the least to say for itself. */
button.quiet.down,
.row .more.down {
  background: var(--hover);
  color: var(--ink);
  box-shadow: var(--down);
  transform: none;
}

/* The third kind. The first two act on what is in the folder; this one changes
   how the folder is looked at, and what says it is the lesser one is the fill
   -- the same way the fill says which of them matters most. It stands at the
   same height as the others: a control that is quieter is not a control that
   is lying down. */
button.setting {
  background: var(--key-quiet);
  color: var(--ink-muted);
  box-shadow: var(--up-quiet);
}
button.setting:hover {
  border-color: var(--edge-low);
  color: var(--ink);
  box-shadow: var(--up-quiet), 0 3px 10px rgb(0 0 0 / 0.06);
}
button.setting:active, button.setting.down {
  box-shadow: var(--down);
  color: var(--ink);
}

/* How many the button is about to act on -- as a badge on the corner rather
   than as another word inside. Every button in this row is the same square;
   one that grows because it has a figure to show is the odd one out, and on a
   phone it pushed the row wider than the rest. */
.toolbar .actions { position: relative; }
.toolbar .actions button { position: relative; }
.toolbar .actions .count {
  position: absolute;
  right: -4px;
  bottom: -4px;
  min-width: 18px;
  height: 18px;
  padding-inline: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 9px;
  background: var(--ink);
  color: var(--raised);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  line-height: 1;
}

/* A control that is only an icon is square, so its hit area is the tap size
   both ways rather than a wide strip. */
button.icon { min-width: var(--tap); padding-inline: var(--s2); }

/* Destructive, and said in the one colour kept for it. */
button.danger { color: var(--danger); }
button.danger:hover { border-color: var(--danger); }

/* Nothing to do, and drawn as nothing to do: no lift, because it cannot be
   pressed, and no hover, because hovering it changes nothing. */
button:disabled {
  opacity: 0.45;
  cursor: default;
  box-shadow: none;
}
button:disabled:hover { border-color: var(--line); background: var(--key); }

input {
  font: inherit;
  font-size: var(--t-md);
  color: var(--ink);
  /* A field is a recess rather than a lid -- the same light as a key that is
     held down, because it is the same thing: somewhere that goes in. */
  background: var(--raised);
  border: 1px solid var(--edge);
  border-top-color: var(--edge-low);
  border-radius: var(--radius);
  box-shadow: var(--down);
  min-height: var(--tap);
  padding: var(--s2) var(--s3);
  width: 100%;
}
input:hover { border-color: var(--edge-low); }

/* A field that has the caret says so with the stripes along its bottom edge
   and a darker border, rather than a blue ring floating around the outside.
   The ring is right for a button, which has no other way to say it; a field
   is a line you write on, and a line under it is the older answer.

   It is also the same thing the rule says: colour marks where the machine is
   engaged. Everything else keeps the ring -- a keyboard has to be able to find
   a button, and a border alone would be a colour-only signal. */
input:focus,
input:focus-visible {
  outline: none;
  border-color: var(--ink);
  background-image: var(--rainbow);
  background-size: 100% 3px;
  background-repeat: no-repeat;
  background-position: bottom;
}

/* ---------- sign in ---------- */

.login {
  max-width: 21rem;
  margin: 14vh auto 0;
  padding-inline: var(--s4);
  display: grid;
  gap: var(--s3);
}
.login .brand { font-size: var(--t-lg); }
.login label {
  display: grid;
  gap: var(--s1);
  font-size: var(--t-sm);
  color: var(--ink-muted);
}

/* ---------- wordmark ---------- */

.brand {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-weight: 600;
  letter-spacing: -0.01em;
}
/* On the button that opens the places, the mark stands where a symbol stands
   and takes the ink of the control around it. No lift: it is pressed, and a
   thing that is pressed does not also hover. */
.burger .mark { filter: none; }

/* The mark sizes itself from the height it is given and its own proportions --
   it is a cloud, not a square, so pinning both sides would squash it.

   The same lift as the buttons, but as a filter rather than a shadow: a
   box-shadow would offset the square the cloud is drawn in, and the mark is
   the cloud, not the square. drop-shadow follows the silhouette, and takes the
   same three values -- so the offset stays one decision for the whole
   interface. */
.brand .mark { flex: none; filter: drop-shadow(var(--cast)); }
.brand b { font-weight: 600; }
.brand span { color: var(--ink-faint); font-weight: 400; }

/* ---------- shell ---------- */

/* One child now: the content. The panel and the mark are taken out of the flow
   and float above it, so the shell has nothing left to divide. */
.shell {
  display: grid;
  grid-template-rows: 1fr;
  min-height: 100dvh;
}

/* No bar along the top any more. What was in it -- the mark, who is signed in,
   the allowance -- is in the panel that slides in from the right, and the
   sixty pixels it cost on every screen are list again.

   The rule sits at the top edge of the content instead, full width: it is the
   first thing on the page and has nothing above it to belong to. */
main {
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: start;
  min-height: 0;
}
.content {
  padding: var(--s4);
  /* Nothing stands along the bottom edge any more, so the list simply ends
     where the page does. The sixty-four pixels this used to hold free for a
     bar of places are the white space that came back. */
  padding-bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px));
  display: grid;
  gap: var(--s3);
  align-content: start;
  width: 100%;
  max-width: 72rem;
  margin-inline: auto;
}
/* A grid track is at least as wide as the widest thing in it unless the thing
   is told it may be narrower. Without this the toolbar reported the full
   folder name as its minimum and stretched the page out from the inside --
   the name never shortened, the page just got wider than the window. */
.content > * { min-width: 0; }

/* ---------- everywhere you could go ---------- */

/* Two columns inside the band: the folders above you on the left, the places
   that are not folders on the right. The hierarchy grows with the depth and
   the others never grow at all, so side by side the block keeps its shape
   whatever folder somebody is standing in.

   One column below 30rem, because two columns of four words on a narrow phone
   are two columns of ellipsis. */
.wayfinder {
  display: grid;
  /* Not down the middle. The right-hand column is four fixed words and never
     needs more than "Papierkorb" plus its symbol; the left one grows with
     every folder somebody stands in. Splitting the width evenly gave the half
     that cannot grow the same room as the half that must. */
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: var(--s2);
  padding: var(--s2) var(--s3) var(--s3);
  align-items: start;
}
/* The folders have a ceiling and then they scroll. The chain was as tall as
   the depth and stopped there; the top level is as tall as somebody's account,
   and a band that opens to the height of the screen is a page, not a band.
   Half the window, the same measure a text file gets for the same reason. */
.band .wayfinder .places:not(.elsewhere) {
  max-height: 50vh;
  overflow-y: auto;
}

/* Measured rather than guessed: at 390 the two columns come to 170 each and
   the longest word in them, "Papierkorb", needs 130 with its symbol. Below 360
   they stack, because two columns of ellipsis are worse than one of words. */
@media (max-width: 22.5rem) {
  .wayfinder { grid-template-columns: 1fr; }
  .places.elsewhere { border-top: 1px solid var(--edge); padding-top: var(--s2); }
}

/* ---------- a page that is not a folder ---------- */

/* One column with air between. Nothing here is scanned -- it is read once and
   acted on rarely. */
.sheet {
  display: grid;
  align-content: start;
  gap: var(--s4);
}
/* No heading of its own: the toolbar at the top says where you are, the same
   as on a folder, and saying it twice would be two titles for one room. */
.sheet .toolbar { padding: 0; }
.sheet .whoami { padding-inline: 0; }
/* A secret, where it was made.
   
   Set off by a rule and a little air rather than a box: it is the answer to
   the control above it, not a separate thing on the page, and a filled panel
   would say otherwise. It goes away when the page is left. */
.secret-out {
  display: grid;
  gap: var(--s2);
  width: 100%;
  padding-block: var(--s2);
}
.secret-out .naming { padding: 0; }
.secret-out .hint { margin: 0; }

/* The forms inside a section stand open rather than unfolding from the strip
   at the top.
   
   The strip is right where the list is already under your eyes -- a folder.
   The account page is scrolled through, and a control that answers at the top
   of it answers where nobody is looking. Nobody is here often enough for the
   saving to be worth that. */
.sheet .section .naming { padding: 0; width: 100%; }

/* How to reach the account from a file manager: the address, then one line
   per system.
   
   A description list rather than a table or a row of cards, because that is
   what it is -- three terms and what each one means. The system names line up
   so the eye finds the right one without reading the others. */
.mount { display: grid; gap: var(--s3); width: 100%; }
/* The address, and the button that takes a copy of it. Baseline-aligned rather
   than centred: the button is taller than the line it sits in, and centring it
   lifts the address off the row its neighbours share. */
.mount .copyable {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}
.mount .copyable span { overflow-wrap: anywhere; }
.mount .copyable button { flex: none; min-width: auto; min-height: auto; padding: var(--s1); }
.mount .how {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--s2) var(--s3);
  margin: 0;
  font-size: var(--t-sm);
}
.mount .how dt { color: var(--ink-muted); }
.mount .how dd { margin: 0; }
/* On a telephone the two columns become two lines: a term of eleven characters
   beside a sentence leaves the sentence four words wide. */
@media (max-width: 30rem) {
  .mount .how { grid-template-columns: 1fr; gap: var(--s1) 0; }
  .mount .how dd { margin-bottom: var(--s2); }
}

/* One subject inside a sheet: a heading, a line saying what it is for,
   whatever it is about, and the button that acts on it.

   The account page holds three -- who you are and how full it is, the
   password, the devices -- and they are three subjects rather than one list,
   which is the difference between a page and a form. Separated by space rather
   than by rules: a line between every pair of things makes a page look like a
   receipt.

   The heading is small and quiet. It separates, it does not announce; the
   toolbar above has already said which room this is. */
.sheet .section {
  display: grid;
  gap: var(--s2);
  justify-items: start;
  padding-block: var(--s4);
  border-top: 1px solid var(--line-soft);
}
.sheet .section h2 {
  margin: 0;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-muted);
}
/* The explanation under a heading reads like the one in a band, because it is
   doing the same job: saying what this costs before somebody presses it. */
.sheet .section .hint { margin: 0; }
/* Whatever the section is about spans the column; only the button sits at its
   own width, where a button belongs. */
.sheet .section .listing { width: 100%; }

/* Centred with room around it is right in a folder, where the empty line
   stands in for the whole listing. In a sheet it stands under a heading, in a
   column of paragraphs, and it should read as one of them. */
.sheet .empty {
  padding: 0;
  text-align: start;
  font-size: var(--t-sm);
}

/* The allowance as a bar. It was a ring around a floating mark, which was the
   one place it could be read at a glance; a bar reads better and says more,
   and it is here, where somebody who wants to know comes to ask. */
.gauge {
  height: 6px;
  border-radius: 999px;
  background: var(--line-soft);
  overflow: hidden;
}
.gauge span {
  display: block;
  height: 100%;
  background: var(--ink);
  transition: width 260ms ease;
}
.gauge.warn span { background: var(--danger); }

.whoami {
  padding: var(--s4) var(--s4) var(--s3);
  display: grid;
  gap: var(--s1);
}
.whoami .name { font-weight: 600; }
.whoami .fact {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--t-sm);
  color: var(--ink-muted);
}
.whoami .fact svg { flex: none; }
.whoami .fact .share { color: var(--ink); font-weight: 500; }
.whoami .fact.warn,
.whoami .fact.warn .share { color: var(--danger); }

/* The folder rows, wherever they are shown. They were the body of a side
   panel and are now the body of the strip that unfolds from the name. */
/* min-width on both, and neither is decoration.

   A grid item is at least as wide as its content unless it is told otherwise,
   and the row below sets its name to one line that does not wrap. Without
   these, a long folder name at depth makes the row wider than its track, the
   track wider than its share, and the hierarchy runs out from under itself and
   across the column beside it. The same trap the stage and the toolbar each
   hit once; it is always this. */
.places { display: grid; align-content: start; padding: var(--s1); gap: 2px; min-width: 0; }
.places .place {
  min-width: 0;
  display: flex;
  justify-content: flex-start;
  gap: var(--s2);
  padding: var(--s2);
  border: none;
  background: none;
  box-shadow: none;
  border-radius: var(--radius-sm);
  color: var(--ink);
  text-align: left;
}
/* One line, cut with an ellipsis. Wrapping was the other half of the problem:
   a folder with a long name at any depth turned its row into three lines, so
   the column that was already short of width was also the tallest thing in
   the band. */
.places .place .fill {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The turn is structure rather than content, so it is drawn a shade back from
   the name it points at. The folder at the root keeps the full weight: it is
   the one row in the column that says what kind of thing all of them are. */
.places .place.deeper > svg { color: var(--ink-faint); }

/* The right-hand column. It sets no columns of its own -- the block above it
   does that -- and it carries a rule only when the two columns have stacked. */
.places.elsewhere { padding: 0; }
.places .place:hover { background: var(--hover); }
/* Where you already are is stated, not offered. */
.places .place.on { font-weight: 500; }
/* Where you are, inside a strip that is itself a shade off the page.

   Pressed in by one pixel, with a floor. It was the two hairlines alone for a
   while, on the argument that the row should keep the colour of the strip it
   is pressed into -- and that was right about the fill it replaced, which sat
   a hundredth away from the strip and marked nothing. Four steps is not a
   hundredth: the groove keeps its shape and the floor says which of four
   things is the one in force without anybody having to look for the shadow. */
.places .place,
.choices .choice {
  /* The floor and its two hairlines come and go rather than cutting. Choosing
     a different sort is one thing moving to another, and at a tenth of a
     second that is what it looks like. */
  transition: background-color 120ms ease, box-shadow 120ms ease;
}
.places .place.on,
.choices .choice.on { background: var(--sunk); box-shadow: var(--down-soft); }

/* ---------- what is playing ---------- */

/* Above everything else in the strip, and on every page, because the sound is
   on every page: it lives outside the component tree and goes on through a
   folder change, the trash and the account. The bar is only a view of it.

   The same chrome as the band it sits on, so the strip reads as one block
   rather than as a thing with a hat on. */
/* The transport opens and closes like everything else in this strip. It used
   to appear outright, and in a strip where the band and the line both unfold,
   the one thing that simply arrived read as the one thing that was broken. */
.player {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 380ms cubic-bezier(0.22, 1, 0.36, 1);
}
.player.open { grid-template-rows: 1fr; }
.player-inner { overflow: hidden; }

/* The player: a display, and the transport under it. */
.transport {
  background: var(--chrome);
  border-bottom: 1px solid var(--line-soft);
  padding: var(--s2);
  display: grid;
  gap: var(--s2);
}
/* What is playing, sunk into the strip.
   
   The same recess the row you are standing in wears in the column beside it,
   for the same reason: it is a thing that is in force rather than a thing to
   press. Full width, one line, cut with an ellipsis -- a title that wrapped
   would move the transport under it every time the song changed, which is the
   one thing a bar over every page must not do. */
.transport .track {
  background: var(--sunk);
  border-radius: var(--radius-sm);
  box-shadow: var(--down);
  padding: var(--s1) var(--s3);
  font-size: var(--t-sm);
  color: var(--ink-muted);
  display: flex;
  /* Not baseline: the name is in a clipped box now, and the baseline of one of
     those is its bottom edge, which put the figures a few pixels low. */
  align-items: center;
  /* A grid item is at least as wide as its content unless it is told
     otherwise. The clipping used to sit here and did that job as a side
     effect; with the clipping a level down, a title of fifty characters made
     the display 436 pixels wide in a 390 pixel window. */
  min-width: 0;
  gap: var(--s2);
}
.transport .track .what {
  /* Held here rather than among the tokens at the top: nothing else in the
     interface moves this way, and the number has to agree with NAME_SLIDE_MS
     in the script, which is easier to see when both are named once. */
  --slide: 260ms;
  --slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
  /* The two names cross inside this box and nowhere else: the one going out is
     clipped at the edge of the display rather than sliding over the figures
     beside it. */
  overflow: hidden;
}
.transport .track .line {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Out of the flow while it lasts, so the display keeps the height of one line
   and the figures do not move while the two names pass each other. */
.transport .track .line.out {
  position: absolute;
  inset: 0;
}
/* Forwards the new title rises and the old one leaves through the top; back
   the other way round. 260 ms, the same number the script holds the outgoing
   line for.

   One curve for both, because the two lines are one strip: give the one
   arriving an ease-out and the one leaving an ease-in and they close the gap
   between them halfway through, which reads as a cross-fade rather than as
   something moving past. Same curve, same length, and they stay exactly one
   line apart the whole way -- which is what makes it a display that turns
   rather than a title that changes. */
.transport .track .what.ahead .line.here { animation: name-from-below var(--slide) var(--slide-ease); }
.transport .track .what.ahead .line.out { animation: name-to-above var(--slide) var(--slide-ease) forwards; }
.transport .track .what.back .line.here { animation: name-from-above var(--slide) var(--slide-ease); }
.transport .track .what.back .line.out { animation: name-to-below var(--slide) var(--slide-ease) forwards; }
@keyframes name-from-below {
  from { transform: translateY(100%); }
  to { transform: none; }
}
@keyframes name-to-above {
  from { transform: none; }
  to { transform: translateY(-100%); }
}
@keyframes name-from-above {
  from { transform: translateY(-100%); }
  to { transform: none; }
}
@keyframes name-to-below {
  from { transform: none; }
  to { transform: translateY(100%); }
}
/* The figures, in the face a machine reads out in. Mono here and nowhere else
   in the strip: it is the one place where the digits change under your eye,
   and a proportional face makes that a wobble. */
.transport .track .when {
  flex: none;
  font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
  font-size: var(--t-xs);
  color: var(--ink-faint);
}
.transport .track .of { padding: 0 0.35em; opacity: 0.55; }
.playing {
  display: flex;
  align-items: center;
  gap: var(--s1);
}
/* And it stands apart from what follows it. Everything to the right of this is
   one cluster -- the bar and the three quiet keys -- and the one key that is
   not part of that cluster should not sit inside it. A margin rather than a
   wider gap, which would have pushed the cluster apart as well and taken the
   width out of the bar. */
.playing > button:first-child { margin-right: var(--s2); }

/* The bar somebody drags. A real range input, given the interface's own
   surfaces rather than the platform's: a recess for the track, the same one
   pit a key falls into, and a key on top of it for the handle. */
.playing input[type="range"] {
  flex: 1;
  min-width: 0;
  appearance: none;
  /* Undone one by one, because the rule for a field applies to this as well
     and a field is the opposite of what this is: a range has no box, no
     recess of its own and no edge -- its track is the recess. Without this it
     arrived wearing a text field. */
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  min-height: 0;
  height: var(--tap);
  width: auto;
  margin: 0;
  cursor: pointer;
}
.playing input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--key-down);
  box-shadow: var(--down-soft);
}
.playing input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--key-down);
}
.playing input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--key-ink);
  border: none;
}
.playing input[type="range"]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: none;
  border-radius: 50%;
  background: var(--key-ink);
}
.playing input[type="range"]:disabled { opacity: 0.4; cursor: default; }
/* A control that leads nowhere says so by fading rather than by vanishing:
   the bar keeps its shape at the ends of a queue. */
.playing button:disabled { opacity: 0.3; }
/* The key that is pressed constantly keeps its full size; the three beside it
   are narrower, because a row this dense cannot give five controls the same
   forty-four pixels and still be a bar. They keep the height, which is the
   half of the tap size that a thumb aims with here. */
.playing button.quiet.icon { min-width: 36px; padding-inline: var(--s1); }

/* The song that is playing, in the list it came from. The symbol changes; the
   name takes the weight. Nothing moves.

   The mark in the row and the key in the bar answer two different questions
   and so they do not always agree: the key says what pressing it would do --
   a triangle while paused -- and the row says what is true, which is that this
   one is paused. Held apart by weight rather than by shape, so the two never
   look like an argument: sounding is full ink, paused steps back. */
.row.sounding .stem { font-weight: 500; }
.row .media.kind.sounding { color: var(--ink); }
.row .media.kind.sounding.held { color: var(--ink-faint); }

/* And the stripes breathe while it runs -- see the note in the strip. */
.rule.sounding::after {
  opacity: 0.45;
  transform: none;
  animation: breathe 3s ease-in-out infinite;
}
/* Work wins over music: a sweep and a breath cannot both be true, and the
   sweep is the one somebody is waiting on. Written after, so it does. */
.rule.busy::after {
  opacity: 1;
  transform: translateX(-100%);
  animation: sweep 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

/* ---------- status strip ---------- */

.strip { flex: none; }

/* The state line at the top of the content, and the only coloured thing in the
   interface. Colour means this line is live right now, in three tenses:

     sweeping   work is running, with no end in sight
     filling    work is running and knows how far it has got
     breathing  the strip is open and waiting for you

   Grey the rest of the time. A permanent rainbow at the edge of every screen
   says nothing, and after a day nobody sees it. */
.rule {
  position: relative;
  height: 3px;
  background: var(--line-soft);
  overflow: hidden;
  flex: none;
}
.rule::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--rainbow);
  transform: translateX(-100%);
  opacity: 0;
}
/* Work with no end in sight: the stripes travel. */
.rule.busy::after {
  opacity: 1;
  animation: sweep 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Open for input: the stripes stand still, faint, and breathe. Far slower than
   the sweep -- three seconds against one and a half -- so that "waiting for
   you" and "working" are never mistaken for each other out of the corner of
   an eye. */
.rule.waiting::after {
  opacity: 0.3;
  transform: none;
  animation: breathe 3s ease-in-out infinite;
}

/* Work with a figure attached: they stand still and fill to it. */
.rule.measured::after {
  opacity: 1;
  animation: none;
  transform: none;
  clip-path: inset(0 calc(100% - var(--p, 0%)) 0 0);
  transition: clip-path 200ms cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}

@keyframes breathe {
  0%, 100% { opacity: 0.22; }
  50% { opacity: 0.6; }
}

/* Opening to the height of the content without knowing that height: a grid row
   goes from no fraction to one, which the browser can interpolate. Animating
   max-height means guessing a number, and the guess either cuts the content
   off or eases towards a value nothing ever reaches. */
.status {
  display: grid;
  grid-template-rows: 0fr;
  background: var(--chrome);
  /* Slow out, no bounce: the strip appears beside something someone is
     reading, and anything quicker reads as a flicker. */
  transition: grid-template-rows 380ms cubic-bezier(0.22, 1, 0.36, 1);
}
.status.open { grid-template-rows: 1fr; }
.status-inner {
  overflow: hidden;
  opacity: 0;
  /* Held open until the close has finished playing, then hidden outright.
     Opacity alone leaves the controls in the tab order at full size inside a
     container marked aria-hidden -- a keyboard lands on things nobody can see,
     and the page has told assistive technology they are not there. */
  visibility: hidden;
  /* Two different journeys, not one played backwards. Opening, the room comes
     first and the content fades in behind it -- the delay is what makes that
     read as unfolding. Closing, the same delay left the content at full
     strength while the row had already taken away two thirds of its height:
     it was being cut off rather than put away. So on the way out it goes
     first, and quickly, and the row closes over nothing. */
  transition:
    opacity 140ms ease,
    visibility 0s linear 380ms;
}
.status.open .status-inner {
  opacity: 1;
  visibility: visible;
  transition: opacity 260ms ease 60ms, visibility 0s linear 0s;
}

/* The band, on the same mechanism as the status line above it and for the same
   reason: a grid row from no fraction to one is a height the browser can
   interpolate without anyone having to guess what that height will be. */
.band {
  display: grid;
  grid-template-rows: 0fr;
  background: var(--chrome);
  transition: grid-template-rows 380ms cubic-bezier(0.22, 1, 0.36, 1);
}
.band.open { grid-template-rows: 1fr; }
/* The one place a hairline is still needed: between a band and a status line
   open at the same time. Everywhere else the rule is the edge. */
.band.open + .status.open { border-top: 1px solid var(--line-soft); }
.band-inner {
  overflow: hidden;
  opacity: 0;
  /* Held open until the close has finished playing, then hidden outright.
     Opacity alone leaves the controls in the tab order at full size inside a
     container marked aria-hidden -- a keyboard lands on things nobody can see,
     and the page has told assistive technology they are not there. */
  visibility: hidden;
  /* Out first and quickly; see the status line above for why the way in and
     the way out are not the same journey. */
  transition:
    opacity 140ms ease,
    visibility 0s linear 380ms;
}
/* Lined up with the list underneath rather than with the window: on a wide
   screen the content is a centred column, and a band that ignores that is a
   band that belongs to a different page. */
.band-inner > *,
.status-line { width: 100%; max-width: 72rem; margin-inline: auto; }
.band.open .band-inner {
  opacity: 1;
  visibility: visible;
  transition: opacity 260ms ease 60ms, visibility 0s linear 0s;
}

/* What the band is, over what it holds.

   Left where the leading symbol of the row below it stands, so the heading and
   the thing it names share an edge -- and no bottom padding of its own: the row
   under it brings its own air, and two of them stacked put the heading nearer
   the rule above than the thing it belongs to.

   A rubric rather than a headline: smaller than the contents and a shade back
   from them. It is naming something that is already on the screen, not
   announcing something that is about to arrive. */
.band-title {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  padding: var(--s3) var(--s4) 0;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-muted);
}
/* The band's symbol lives here and only here, which is what lets every line
   under it start at the same edge as this one. */
.band-title > svg { flex: none; }

/* What the band costs, directly under what it is.

   No symbol of its own: the heading above it already carries one, and a second
   one two lines down turns a pair into a list. The separation it needs is the
   one it has -- heading and sentence at the top, controls below, and the
   controls start where a control starts rather than where a paragraph does.

   Held back from the controls in weight and size, because it is read once and
   they are used every time. */
.band-note {
  margin: 0;
  padding: var(--s1) var(--s4) var(--s3);
  font-size: var(--t-sm);
  color: var(--ink-faint);
  max-width: 42rem;
}

/* A name being typed: what it is, the field, and the two answers. The field
   keeps the shape it had in the list -- white, with room around the text and
   around itself. */
.naming {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3) var(--s2) var(--s4);
}
.naming > svg { flex: none; color: var(--ink-muted); }
.naming input {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--raised);
  padding: var(--s2) var(--s3);
}
.naming input::placeholder { color: var(--ink-faint); }

/* A question wears the same line as a name being typed, because it is the same
   thing: the band asking for something before it acts. */
.naming.asking > svg { color: var(--danger); }
/* A field that can be emptied carries its cross inside itself.
   
   The box around the input exists only to hang that cross on: the input keeps
   the look every other field in the interface has, and the button sits over
   its right end rather than beside it in the row. Room is left for it whether
   or not it is there, so the text never reflows on the character that brings
   it. */
.naming .typing {
  position: relative;
  display: flex;
  flex: 1;
  min-width: 0;
}
.naming .typing input { flex: 1; min-width: 0; }
.naming.searching .typing input { padding-right: calc(var(--s3) + 14px + var(--s2)); }
.naming .wipe {
  position: absolute;
  right: var(--s2);
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--s1);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  box-shadow: none;
  color: var(--ink-faint);
  cursor: pointer;
  min-height: 0;
}
.naming .wipe:hover { color: var(--ink); background: var(--hover); }
/* Wraps rather than shortens. A name can lose its end and still be the name;
   a question that loses its end is not a question. */
.naming .question {
  flex: 1;
  min-width: 0;
  font-size: var(--t-sm);
}

/* Giving a folder away: one line to decide, one line to say what it costs.
   The line underneath is part of the control, not a footnote -- whose
   allowance the bytes land on is the thing nobody guesses right. */
.sharing { display: grid; }
/* Wraps, because four controls and a name do not fit across a telephone. The
   select keeps the first line to itself there and the three buttons take the
   second; at any width where they all fit, they all stay on one. */
.sharing .naming { flex-wrap: wrap; }
/* Only the gap between the lines is taken out. The indent that used to be
   here went away with the symbol it was avoiding: with the symbol in the
   heading there is nothing in the first line to start under, and all three
   lines share the edge the heading sets. */
.sharing .naming.second { padding-top: 0; }
/* As wide as what it holds, not as wide as a guess.

   It was fixed at 9rem, and a native select draws its arrow against its right
   edge: with "Benjamin" in a 144px box the arrow stood an inch away from the
   word it belongs to, reading as a rendering fault rather than as a control.
   Left to size itself, a select takes the width of its widest option, so the
   box stays put when the choice changes and the arrow sits where the text
   ends. */
.sharing .who { flex: 0 1 auto; }
/* The one-line dialogue: the name takes what the other two leave.

   nowrap, because the three are one sentence and a sentence that wraps here
   has room for neither half -- and because the button is 44px whatever
   happens, so what gives is the field, which can. */
.sharing .naming.one-line { flex-wrap: nowrap; }
.sharing .naming.one-line input { flex: 1 1 4rem; min-width: 0; }

.sharing .who {
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--raised);
  padding: var(--s2) var(--s3);
  font: inherit;
  color: inherit;
  /* Tall enough to hit, like every other control in this band. */
  min-height: var(--tap);
}

/* A text file, read in the band.
   
   Half the window and then it scrolls: enough that a note is read in one
   glance and a list in two, and never so much that the folder underneath
   disappears -- the band is a thing held in front of the list, not a page of
   its own. */
.reading { display: grid; }
.reading .naming { padding-bottom: var(--s2); }
.reading .hint { padding: 0 var(--s4) var(--s3); display: flex; gap: var(--s2); }
.reading .hint svg { flex: none; margin-top: 2px; }
.text {
  margin: 0;
  max-height: 50vh;
  overflow: auto;
  padding: 0 var(--s4) var(--s3);
  font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
  font-size: var(--t-sm);
  line-height: 1.5;
  /* Wrapped rather than scrolled sideways. A line of prose in a note is
     longer than a telephone and means to be read; a line of data that is
     wrapped is still all there, which is more than can be said for one that
     has run off the right-hand edge. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  -webkit-overflow-scrolling: touch;
}

/* A freshly made address, in the band where everything else is asked and
   answered. The square first and centred, the address under it, and the one
   sentence about it at the end -- downwards, that is the order somebody needs
   it in. No panel of its own: the band already is one. */
.fresh {
  display: grid;
  justify-items: center;
  gap: var(--s3);
  padding: var(--s3) 0 var(--s2);
}
/* The centring is for the square, which is the one thing here that is looked
   at rather than read. The sentence under the heading is read, and it starts
   where every other line in every other band starts. */
.fresh .band-note { justify-self: stretch; }
.fresh .naming { width: 100%; padding-inline: var(--s4) var(--s3); }
/* The row is the address and the two things one does with it, so the address
   gets whatever is left. It may lose its last characters on a narrow screen,
   and that is the right thing to lose: nobody reads an address off a screen
   -- they scan the square, press copy, or press share. */
.fresh .address { flex: 1 1 8rem; }
.fresh .naming { flex-wrap: wrap; }

/* The square at about three centimetres on a telephone. Two was the size a QR
   code is printed at on a card, and on a screen held up between two people it
   turned out to be mean -- the code carries a mark in the middle now, which
   makes it denser, and half again is what that is worth.
   
   Not in CSS centimetres, which are not centimetres on a telephone: the unit
   is anchored to 96 pixels per inch and a phone is nearer 140, so 3cm would
   draw about 1.8. Measured instead -- 390 CSS pixels across a screen 71.5 mm
   wide is 0.183 mm each, so 168 of them are 31 mm. */
.qr {
  width: 10.5rem;
  height: auto;
  border-radius: var(--radius-sm);
  /* Crisp edges rather than a smoothed grey fringe, which is what a scanner
     trips over at small sizes. */
  image-rendering: pixelated;
  shape-rendering: crispEdges;
}
/* ---------- the second factor ---------- */

/* Enrolment, in the order it is done: point a camera at the square, or type
   the key if there is no camera, then prove it arrived. */
.enrolling {
  display: grid;
  gap: var(--s3);
  justify-items: start;
}
/* The key, for the case the camera cannot be used -- a desktop authenticator,
   or the telephone that is photographing this screen. Selectable in one press
   because it is thirty-two characters nobody wants to drag across. */
.enrolling .key {
  display: block;
  width: 100%;
  background: var(--sunk);
  box-shadow: var(--down);
  border-radius: var(--radius-sm);
  padding: var(--s2) var(--s3);
  font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
  font-size: var(--t-sm);
  color: var(--ink-muted);
  word-break: break-all;
  user-select: all;
}
.enrolling .naming.one-line { width: 100%; padding: 0; }

/* What is left when it is on: one line of fact and the password that guards
   the two ways to weaken it. */
.factor-on { display: grid; gap: var(--s3); }
.factor-on .fact {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  color: var(--ink-muted);
}
.factor-on .naming.one-line { padding: 0; }

/* The recovery codes, in a block to be printed or copied whole rather than in
   eight fields to be copied one at a time. */
.codes {
  list-style: none;
  margin: 0;
  padding: var(--s3);
  background: var(--sunk);
  box-shadow: var(--down);
  border-radius: var(--radius-sm);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: var(--s2) var(--s3);
  font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
  font-size: var(--t-sm);
  user-select: all;
}

.naming .address,
.naming .word {
  flex: 1 1 14rem;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--raised);
  padding: var(--s2) var(--s3);
  font: inherit;
  color: inherit;
}
.naming .address { font-size: var(--t-sm); }

/* Two rows of choices. Each group is one line, so which of four is in force is
   read across rather than scrolled down -- the shape a popup could not have
   and the reason for moving them here. */
/* Two columns, each a stack of equal width. Pills as wide as their words wrap
   where the words happen to run out, which left seven different widths in two
   ragged rows; a column gives every choice the same edge and the pair of them
   gives the band a rectangle. */
.choices {
  padding: var(--s2) var(--s4) var(--s3);
  display: grid;
  /* Two columns that stop growing, in a block that does not. Capping the block
     instead put it back in the middle of the band's own column -- 240 pixels
     from where the first tile starts at 1280 -- because the rule that gives
     every child of the band the content's column does it with an auto margin
     on each side. The columns are what needs a ceiling; beyond twenty rem they
     are wider than anything in them. */
  grid-template-columns: repeat(2, minmax(0, 20rem));
  justify-content: start;
  gap: var(--s3);
}
.choices .group { display: grid; align-content: start; gap: 2px; }
.choices .choice {
  justify-content: flex-start;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  min-height: var(--tap);
  background: none;
  border-color: transparent;
  box-shadow: none;
  font-size: var(--t-sm);
  color: var(--ink-muted);
}
.choices .choice svg { flex: none; }
/* The space an arrow would take, kept on the keys that have none, so the three
   names stay in one line down the column. */
.choices .slot { flex: none; width: var(--i-md); }
.choices .label { flex: none; }
/* The direction in words after the name. It is the part that gives way when
   the column is too narrow for both -- the name is what the row is. */
.choices .way {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-faint);
  font-weight: 400;
}
.choices .choice:hover { background: var(--hover); border-color: transparent; }
/* The one in force is a surface; the rest are text. That is the same
   distinction the three kinds of button make, one step quieter. */
/* The one in force is a key held down, the same as the place you are standing
   in and the same as a control whose strip is open. It used to stand raised
   out of the flat rows around it, which said "press me" about the one thing
   already pressed. */
.choices .choice.on {
  color: var(--ink);
  font-weight: 500;
}
.choices .choice .note { color: var(--ink-faint); font-weight: 400; }

.status-line {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s4);
  font-size: var(--t-sm);
}
/* Icons drawn larger than the size a control gives them: the picture in a row
   that has no preview, and the one filling a tile. Tabler draws on a 24 unit
   grid, so scaling the drawing to 38 or 43 pixels scales its 1.75 unit stroke
   to nearly three -- the heaviest lines in an interface of light ones, and on
   a folder, which is the one thing in a list that should not shout. Held
   still, an icon grows and its line keeps the weight every other line has. */
.row .media.kind svg path,
.tile .shot svg path { vector-effect: non-scaling-stroke; }

/* The icon says what the line is about before the words do -- the same device
   as in the meta line of a row, at the same weight, so the two read as one
   vocabulary rather than two. */
.status-line > svg { flex: none; color: var(--ink-muted); }
.status-line.error > svg { color: var(--danger); }
.status-line .share {
  font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
  font-size: var(--t-xs);
  color: var(--ink-muted);
  /* Fixed width, so the text beside it does not shuffle sideways every time
     the figure gains or loses a digit. */
  min-width: 2.8rem;
}
.status-line .what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A status line may carry one action at its end -- putting down what was cut,
   and whatever else turns out to need taking back. */
.status-line button.icon { flex: none; margin-block: calc(var(--s2) * -1); }
.status-line.error .what,
.status-line.error .share { color: var(--danger); }

/* ---------- toolbar ---------- */

/* Where you are on the left, what you can do here on the right. The actions
   belong to the content rather than to the application, so they sit with the
   content. */
/* One line, always. Wrapping was the wrong escape: a flex row that may wrap
   moves an item to the next line rather than letting it shrink, so the moment
   the folder name and the buttons were a few pixels too wide together, the
   buttons went down a line instead of the name giving way. Now the name gives
   way -- it is the only thing here that can be shortened and still be read. */
/* The head of the list stays on screen while the list runs under it.
   
   It pins to whichever thing is scrolling -- the window on a telephone, the
   content column beside the rail at the desk -- because sticky asks that
   question itself and both answers are already right.
   
   The padding and the margin that cancels it are one device: at rest the bar
   sits exactly where it sat, and pinned it carries a band of ground with it,
   so the rows pass behind it rather than against it. Ground rather than
   chrome: it is the head of the page's own column, not a bar laid over it. */
.toolbar {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--ground);
  padding: var(--s2) 0 var(--s3);
  margin: calc(var(--s2) * -1) 0 calc(var(--s3) * -1);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: nowrap;
  gap: var(--s3);
}
.toolbar .where { flex: 1 1 auto; min-width: 0; }
.toolbar .actions { display: flex; align-items: center; gap: var(--s2); flex: none; }

/* ---------- breadcrumb ---------- */

/* One name, and a way up beside it. The trail this replaces grew with the
   depth and with the length of the names in it, and it shared its row with the
   buttons: three levels down it pushed them onto a line of their own. A single
   name cannot, because it shortens. */
.where {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
}
/* Pulled left by its own padding, so the name sits where it would have sat
   without a button in front of it and the row does not shift as you descend. */
.where button { margin-left: calc(var(--s2) * -1); }
/* The three lines keep the ink of a name rather than the grey of a quiet
   control: it is the way into the whole interface, not an afterthought beside
   the title. */
.where .burger { color: var(--ink); margin-left: calc(var(--s2) * -1); }
/* Open, the mark takes the fill of a pressed control, like every other switch
   in this bar. It used to take the fill of the band instead, which was a
   quieter shade than the pressed one and said the softer thing: "this is where
   that came from" rather than "this is on". It also came from a second source,
   the aria-expanded attribute, and two ways of saying pressed is one too
   many -- the class that every other switch uses says it now. */
/* The name is a control now: it unfolds the hierarchy underneath it. Every
   button surface is taken back off -- no edge, no fill, no lift -- because
   this is text with a hit area, like the row it names things in.

   The inner padding is cancelled by an equal negative margin, so the name
   starts exactly where it started when it was a span. A control that shifts
   the thing it replaced by eight pixels is a control somebody notices without
   being able to say what changed. */
.where .here {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  min-height: var(--tap);
  padding-inline: var(--s2);
  margin-inline: calc(var(--s2) * -1);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  box-shadow: none;
  font-size: var(--t-md);
  font-weight: 500;
  color: var(--ink);
}
.where .here .stem {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.where .here:hover { background: var(--hover); color: var(--ink); box-shadow: none; }
/* Open, it takes the fill of the strip it has opened, so the two read as one
   thing rather than as a pressed button with a panel under it. */
.where .here[aria-expanded="true"] { background: var(--chrome); }
.where .here:active { transform: none; box-shadow: none; }

/* ---------- listing ---------- */

/* No frame around the list. On a phone a border with its own padding inside
   the page padding costs two gutters of width for nothing; the separators
   already say where one row ends and the next begins. */
.listing { display: grid; }

.row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "media entry more";
  align-items: center;
  column-gap: var(--s3);
  border-bottom: 1px solid var(--line-soft);
  --media: 38px;
}
.row:last-child { border-bottom: none; }

/* Picked. Filled rather than faded: nothing is leaving yet, and a row at
   forty-five per cent was 2.67:1 for its own name -- on a row you have to be
   able to tap again to change your mind. */
.row.picked { background: var(--hover); }
.tile.picked .shot { border-color: var(--ink); }

/* The mark, opposite the three dots, in the same place in all three views. A
   tick on the picture rather than instead of it: you are choosing between
   pictures, so the pictures have to stay. */
/* On a tile the mark sits in the corner opposite the dots, because the
   picture is the thing being chosen between and has to stay visible. On a row
   the picture is thirty-eight pixels and there is no corner worth the name, so
   the mark sits in the middle of it. */
.row.picked .media::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  width: 22px;
  height: 22px;
  border-radius: var(--radius-sm);
  background: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fbfaf8' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5l10 -10'/%3E%3C/svg%3E");
  background-size: 15px 15px;
  background-position: center;
  background-repeat: no-repeat;
}

.tile.picked .shot::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-sm);
  background: var(--ink);
  /* The tick itself, drawn rather than fetched: one icon in one place does not
     earn a component, and an SVG in a pseudo-element cannot take currentColor
     anyway. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fbfaf8' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5l10 -10'/%3E%3C/svg%3E");
  background-size: 12px 12px;
  background-position: center;
  background-repeat: no-repeat;
}
.tile.picked .shot { position: relative; }

/* Only on devices that can hover. On a touch screen the highlight sticks after
   a tap and makes the last row you touched look selected. */
@media (hover: hover) {
  .row:hover { background: var(--hover); }
  /* Guarded like its neighbours. Outside the guard a tapped name kept its
     underline after the finger had gone, because a touch screen has no way to
     stop hovering. */
  .row .entry:hover .stem { text-decoration: underline; }
}

/* The frame, not the picture, is the grid item. An image as the item sizes the
   row from its own proportions -- height: auto works the height out from the
   width, and height: 100% ties the two together in a circle the browser
   resolves in favour of the image. Either way one portrait preview made its
   row taller than every other one. The frame has no proportions of its own and
   the picture inside is out of the flow, so the row is measured by its text
   and the picture fills whatever that comes to. */
/* The picture is its own target. Not focusable: the row already has one stop
   for the keyboard, and a second one per row for the same thing would double
   the length of the list to tab through. */
.row .pick {
  grid-area: media;
  display: flex;
  align-self: stretch;
  cursor: pointer;
}

.row .media {
  grid-area: media;
  position: relative;
  align-self: stretch;
  width: var(--media);
  /* Square at the least. The frame stretches to whatever the row comes to, but
     it is also what keeps a row from collapsing around a short entry: a folder
     has one line of text and a small icon, and without this its row would be a
     third shorter than the one under it. */
  min-height: var(--media);
  flex: none;
  overflow: hidden;
  border-radius: var(--radius-sm);
  margin-block: var(--s2);
}
.row .media.kind {
  color: var(--ink-faint);
  display: flex;
  align-items: center;
  justify-content: center;
}
.row.dir .media.kind { color: var(--ink-muted); }
/* A drawn icon takes a control's icon size, not the frame's. The frame is
   there to line the column up with the previews beside it; filling it made
   every file without a picture shout as loudly as the photographs. */
.row .media.kind svg { width: var(--i-md); height: var(--i-md); }
.row .thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--hover);
}

/* The whole text block is the target, not the one line of the name.

   A name is a single line of text: twenty-three pixels tall, and that is what
   a finger was being asked to hit. Wrapping both lines in the control gives a
   target the height of the row, which is what someone is aiming at anyway. */
.row .entry {
  grid-area: entry;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* In a column the cross axis is the horizontal one, and every engine gives a
     button a centred cross axis of its own. That is why a folder's name sat in
     the middle of its row while a file's -- a link, with no such default --
     sat at the left. */
  align-items: stretch;
  gap: 2px;
  min-width: 0;
  min-height: var(--tap);
  padding-block: var(--s2);
  /* A button carries padding of its own; a link does not. Left alone, folder
     names stood twelve pixels further in than file names. */
  padding-inline: 0;
  border: none;
  background: none;
  box-shadow: none;
  font: inherit;
  font-size: var(--t-md);
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

/* The name on one line. The stem may shrink and the extension may not, so the
   ellipsis lands before the ending rather than eating it: a list of files is
   read by its endings as much as by its names. */
.row .name {
  display: flex;
  align-items: baseline;
  min-width: 0;
  gap: 0;
}
.row .name .stem {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row .name .ext { flex: none; color: var(--ink-muted); }


/* The line under a name: what the file is, in figures.

   Not monospaced any more. The argument for it was that figures line up in a
   column -- and they never did here, because every figure has an icon in front
   of it and the icons are different widths, so each column starts somewhere
   else anyway. What the mono face did buy was width: it is a good deal wider
   per character than the interface face, on the one line that has the least
   room and now carries a third figure. */
.row .meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s3);
  font-size: var(--t-xs);
  color: var(--ink-faint);
  /* Figures still line up where they can, which is inside one figure rather
     than between them: 1,2 MB and 8,8 MB are the same width, so the date
     after them does not move from row to row. */
  font-variant-numeric: tabular-nums;
}
.row .meta span { display: inline-flex; align-items: center; gap: var(--s1); }

/* Always there, never loud. A row whose actions appear on hover has none on a
   phone, and one with a bright button in every row is a wall of buttons. */
.row .more {
  grid-area: more;
  background: none;
  border: none;
  color: var(--ink-faint);
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  box-shadow: none;
}
.row .more:hover { color: var(--ink); background: var(--hover); }

/* The row where a new folder is named. It borrows the shape of a real row so
   the name is typed exactly where the folder will appear. */
/* ---------- grid ---------- */

/* Tiles rather than rows, for folders that are looked at instead of read.
   The column count is not a breakpoint: it falls out of the smallest tile
   worth showing, which comes to three across on a phone and as many as fit
   after that. */
.listing.grid {
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: var(--s3) var(--s2);
  padding-block: var(--s3);
}

/* Anything that is not a tile runs the full width: the row where a folder is
   named, and the line that says the folder is empty. Neither is one item
   among many. */
.listing.grid > .row,
.listing.grid > .empty { grid-column: 1 / -1; }

/* ---------- gallery ---------- */

/* The same tiles, in columns, at their own proportions. Columns rather than a
   grid, because a grid needs rows and pictures of different heights have none
   in common. The column count comes from the width in JavaScript -- CSS could
   do the columns but not the order, and it would fill the first column top to
   bottom before starting the second, which puts the oldest third of a folder
   down the left edge. */
.listing.masonry { display: block; }
/* One gap, both ways. The column gap was smaller than the gap down a column,
   which was right while every tile carried a caption under it -- the caption
   was part of the space -- and wrong the moment the captions went: the
   pictures then sat closer side to side than one above the other. */
.listing.masonry .columns {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  padding-block: var(--s3);
}
.listing.masonry .column {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  flex: 1 1 0;
  min-width: 0;
}
/* The frame is given its proportions per tile, from the picture once it has
   said what they are. A panel in landscape is for what has no picture. */
.listing.masonry .tile .shot { aspect-ratio: auto; }

/* A strip one control high, not a panel.

   Worn by two things: an entry with no picture to show, and a picture whose
   proportions are not known yet. The second is the more useful of the two --
   see the note in GalleryTile.

   Anything without a picture used to take a landscape panel -- a folder at the
   head of a gallery was as tall as three quarters of a photograph, for an icon
   and a word. The argument for the panel was that it should read as a label
   rather than as a photograph of a folder; that is right, and a label is a
   line of text, which is this. The height is the one every control in the
   interface has, so a row of folders above the pictures reads as a bar rather
   than as a first row of something. */
.listing.masonry .shot.plain {
  aspect-ratio: auto;
  min-height: var(--tap);
  flex-direction: row;
  justify-content: flex-start;
  gap: var(--s2);
  padding-inline: var(--s3);
  background: var(--chrome);
  color: var(--ink-muted);
}
.listing.masonry .shot.plain svg { width: var(--i-md); height: var(--i-md); flex: none; }
.listing.masonry .shot.plain .name {
  display: flex;
  flex: 1;
  min-width: 0;
  font-size: var(--t-sm);
  color: var(--ink);
}
.listing.masonry .shot.plain .stem {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.listing.masonry .shot.plain .ext { flex: none; color: var(--ink-faint); }

.tile { position: relative; min-width: 0; }
.tile .entry {
  display: grid;
  gap: var(--s1);
  min-width: 0;
  /* All of this fights the same thing: a button is not a plain box. It sizes
     itself to its contents even as a block, and every user agent centres what
     is inside it -- so the folder tile came out as a small icon in the middle
     of a column the picture tiles filled, with its name centred under it.
     Spelled out rather than left to the defaults, because the defaults differ
     between engines and the tile has to look the same in all of them. */
  width: 100%;
  grid-template-columns: minmax(0, 1fr);
  justify-items: stretch;
  justify-content: stretch;
  align-content: start;
  border: none;
  background: none;
  box-shadow: none;
  padding: 0;
  font: inherit;
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

/* Square, and the picture cropped to fill it. A grid of photographs in their
   own proportions is a ragged thing to scan, and a tile is the way in to a
   file rather than a way of showing it. */
.tile .shot {
  position: relative;
  aspect-ratio: 1;
  max-width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--raised);
  color: var(--ink-faint);
}
.tile .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The mark that says a still is a film.

   A filled triangle on a pale disc, and named through its containers because
   the rule that sizes a drawn icon inside a tile -- 38 per cent of the frame,
   so a folder among photographs is not a symbol lost in a square -- is two
   classes deep and would win otherwise. This is not that kind of icon: it sits
   on a picture rather than standing in for one. */
.play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  color: var(--ink);
}
.tile .shot .play svg,
.row .media .play svg,
.play svg {
  width: var(--i-md);
  height: var(--i-md);
  box-sizing: content-box;
  border-radius: 50%;
  background: rgb(255 255 255 / 0.82);
  /* A triangle's weight sits left of its box, so the padding is a pixel wider
     on that side: the disc then looks centred on the mark rather than on the
     box the mark is drawn in. */
  padding: 7px 7px 7px 9px;
}
/* In a row the frame is only as tall as the row, so the disc comes down to
   fit inside it. */
.row .media .play svg { width: 15px; height: 15px; padding: 5px 5px 5px 6px; }
/* A tile with no picture in it still has a tile's worth of room. The icon is
   given as a share of that rather than in pixels, so a folder among
   photographs reads as one thing among many instead of a symbol lost in a
   grey square.

   The stroke does not come along for the ride. Tabler draws on a 24 unit grid,
   so scaling the drawing to 43 pixels scales a 1.75 unit stroke to three -- a
   folder drawn in a marker pen next to photographs. Held still, the icon grows
   and its line stays the weight every other line in the interface has. */
.tile .shot svg { width: 38%; height: auto; }
.tile.dir .shot { background: var(--chrome); color: var(--ink-muted); }

.tile .name {
  display: flex;
  justify-content: flex-start;
  min-width: 0;
  font-size: var(--t-sm);
  line-height: 1.3;
}
.tile .name .stem { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tile .name .ext { flex: none; color: var(--ink-faint); }
@media (hover: hover) {
  .tile .entry:hover .stem { text-decoration: underline; }
}

/* On the picture rather than in the caption, where it would take half the
   width a name has. Always there, never loud -- a pale disc under the dots so
   they stay legible on a bright photograph. */
.tile .more {
  position: absolute;
  top: 0;
  right: 0;
  width: var(--tap);
  height: var(--tap);
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  box-shadow: none;
  color: var(--ink);
}
/* A chip, not a disc. Everything else here is a square with three pixels off
   its corners -- a circle floating on a photograph belonged to a different
   interface. */
.tile .more::before {
  content: "";
  position: absolute;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: var(--raised);
  opacity: 0.82;
}
.tile .more svg { position: relative; }
.tile .more:hover::before { opacity: 1; }

/* That a preview is on its way.

   Late on purpose. A preview that already exists on the storage arrives in no
   time, and a mark that flashed for twenty milliseconds on every tile of a
   folder somebody has opened before would be worse than no mark at all. So it
   is not drawn at all for the first four tenths of a second -- done with a
   delay on the animation rather than with a timer in the component, because
   this is a question about drawing and not about state. */
.waiting-mark {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  color: var(--ink-faint);
  font-size: var(--t-sm);
  opacity: 0;
  animation: show 160ms ease-out 400ms forwards;
  pointer-events: none;
}
.waiting-mark svg { animation: turn 900ms linear infinite; }
@keyframes show { to { opacity: 1; } }
@keyframes turn { to { rotate: 360deg; } }

/* Over a photograph's own ground the mark takes the light colour, the way
   everything else in the viewer does. */
.viewer .waiting-mark { color: rgb(255 255 255 / 0.55); }

/* In the gallery it is the content of the strip rather than something laid
   over a box, so it sits in the row the strip already makes -- beside the
   place a folder puts its icon, and reading the same way. */
.listing.masonry .shot.waiting .waiting-mark {
  position: static;
  justify-content: flex-start;
}
/* The picture is in the layout and invisible rather than absent: taken out, a
   lazily loaded image is one the browser may decide not to fetch at all, and
   then the mark would spin for a picture nobody had asked for. */
.listing.masonry .shot.waiting img,
.tile .shot.waiting img,
.row .media.waiting .thumb {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.empty {
  padding: var(--s6) 0;
  color: var(--ink-faint);
  text-align: center;
}

/* A line of explanation above a list. Said once, quietly, where the thing it
   explains is -- not in a dialog nobody reads. */
.hint {
  margin: 0;
  font-size: var(--t-sm);
  color: var(--ink-muted);
  max-width: 42rem;
}

/* ---------- the public link ---------- */

/* A visitor's page. No menu, no allowance, no way anywhere else: one folder,
   or one place to put something down. The column is narrower than the
   application's, because there is no list of hundreds to make room for. */
/* Its own rows, because the shell's single 1fr belongs to the <main> the
   application puts there and a visitor's page has none: without this the
   strip's row grew to the height of the screen and the folder sat under it,
   off the bottom. */
.shell.public { grid-template-rows: auto 1fr; }
.shell.public .content {
  max-width: 44rem;
  margin-inline: auto;
  width: 100%;
  align-content: start;
}
.shell.public .sheet { padding: var(--s4); gap: var(--s4); }
/* The name of the folder, stated rather than offered: it opens nothing. */
.where .here.plain {
  padding: var(--s2) 0;
  background: none;
  box-shadow: none;
  cursor: default;
}
.dropbox { display: grid; gap: var(--s2); justify-items: start; }
/* A link that leads nowhere any more, said plainly and without blame: the
   person reading it did nothing wrong, and the next step is somebody else's. */
.gone {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  margin: 0;
  padding: var(--s4);
  background: var(--chrome);
  border-radius: var(--radius);
  color: var(--ink-muted);
}
.gone svg { flex: none; margin-top: 1px; }

/* The trash states rather than offers: its rows lead nowhere, the menu on each
   is the whole of what can be done with them. */
.row div.entry { cursor: default; }

/* ---------- row menu ---------- */

.menu {
  position: fixed;
  /* Above everything, because it is the only thing that can be opened on top
     of everything. It sat at 20, under the floating mark at 30: a menu opened
     from the last row on the right was drawn underneath it. */
  z-index: 60;
  min-width: 13rem;
  max-width: min(20rem, calc(100vw - var(--s5)));
  display: grid;
  background: var(--raised);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: var(--s1);
  box-shadow: var(--float);
  /* It arrives from the corner it hangs off rather than simply being there.
     A tenth of a second and a hair of scale -- enough that the eye follows it
     back to the dots that opened it, and not so much that a menu somebody
     opens forty times a day becomes something they wait for. */
  transform-origin: top right;
  animation: unfold 120ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes unfold {
  from { opacity: 0; scale: 0.96; }
}
@keyframes rise {
  from { opacity: 0; }
}
/* The head says which thing the menu is about, and it is not allowed to decide
   how wide the menu is. A long file name was pulling the panel out to its
   maximum width -- three hundred and twenty pixels of menu for four short
   words -- because a grid track sizes itself to its widest item. Zero width
   contributes nothing to that measurement; the minimum then fills whatever the
   options settled on. */
.menu-head {
  width: 0;
  min-width: 100%;
  padding: var(--s2) var(--s2) var(--s2);
  font-size: var(--t-xs);
  color: var(--ink-faint);
  overflow: hidden;
  border-bottom: 1px solid var(--line-soft);
  margin-bottom: var(--s1);
}
/* Shortened before the ending rather than after it, the same way a row is: a
   file is known by its ending as much as by its name, and the head is there to
   say which file this menu belongs to. */
.menu-head .name { display: flex; align-items: baseline; min-width: 0; }
.menu-head .stem { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-head .ext { flex: none; }
.menu a,
.menu button {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--s2);
  min-height: var(--tap);
  padding: var(--s2) var(--s2);
  border: none;
  background: none;
  box-shadow: none;
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-md);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.menu a:hover,
.menu button:hover { background: var(--hover); }
.menu button.danger:hover { color: var(--danger); }

/* ---------- notices ---------- */

.notice {
  color: var(--danger);
  font-size: var(--t-md);
  display: flex;
  align-items: center;
  gap: var(--s1);
  margin: 0;
}

/* Dragging over the window draws a frame and nothing else -- an overlay would
   cover the very thing being aimed at. In ink, not in one of the six: the
   stripes belong to the rule, and the rule is already saying it by breathing.
   Two things reporting the same state, each in its own language. */
.dropping { box-shadow: inset 0 0 0 3px var(--ink); }

/* ---------- narrow ---------- */

@media (max-width: 560px) {
  /* The labels go and the icons carry the meaning. The upload button keeps its
     weight, so which of the two matters more is still legible without reading
     a word. */
  .toolbar .actions .label { display: none; }
  /* The count is not a label. On a phone the words go and the figure stays,
     because "delete" and "delete four things" are not the same sentence. */
  .toolbar .actions .count { display: flex; }
  .toolbar .actions button { min-width: var(--tap); padding-inline: var(--s2); }
}

@media (prefers-reduced-motion: reduce) {
  /* Still there, still saying the same thing, not turning. */
  .waiting-mark { animation-delay: 400ms; animation-duration: 1ms; }
  .waiting-mark svg { animation: none; }
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }

  /* Still says the machine is working, without anything moving. Switching the
     animation off alone would have left the rule blank at exactly the moment
     it has something to report. */
  .rule.busy::after { opacity: 1; transform: none; animation: none; }
  /* The measured fill keeps its transition: it is information moving, not
     decoration, and a figure that jumps is harder to read than one that
     slides. */
  .rule.measured::after { transition-duration: 200ms !important; }
}


/* ---------- looking at a picture ----------

   The one place in this interface that is dark, and the one place with no
   chrome around the content: the ground goes out so that the only lit thing on
   the screen is the photograph. It is fitted, never cropped, and it never asks
   to be turned -- a viewer that wants the telephone rotated for every second
   picture is a viewer people stop opening.

   What is shown is the large rendition, not the file. The original is a
   twentieth of a second's work for the server and several seconds of a mobile
   connection, and none of those bytes reach the screen: 1600 pixels is already
   more than the screen has. The file itself is behind the download button, and
   that button says "Original" so nobody is in any doubt about which of the two
   they are taking away. */

body.viewing {
  overflow: hidden;
}

.viewer {
  position: fixed;
  inset: 0;
  /* The dark comes up rather than arriving. A picture opened from a bright
     list is the largest change of state in the interface, and it was the only
     one that happened between two frames. */
  animation: rise 180ms ease-out;
  /* Over everything, the floating account mark and the side panel included:
     nothing on the page underneath is addressable while a picture is up. */
  z-index: 70;
}

.viewer .backdrop {
  position: absolute;
  inset: 0;
  background: var(--dark);
  /* The wash is drawn larger than the box on purpose; this is what keeps the
     overhang out of sight. */
  overflow: hidden;
}

/* The picture's colours, carried on past its own edges.

   Laid out with the same two lines as the frame, so the copy sits exactly
   where the photograph sits, and then grown from its centre: what ends up
   beside the photograph's top edge is what was just inside that top edge.
   That is the whole of the effect. Filling the screen instead -- object-fit
   cover, the obvious way -- crops the picture to the screen's proportions, so
   the colour above the photograph comes from somewhere near its middle and the
   two do not meet.

   Nothing is done about the copy's own outer edge, and that is deliberate: a
   blur pulls in what lies beyond what it blurs, beyond it is nothing, and the
   glow therefore fades into the dark by itself. That fade is the far end of
   the effect rather than a flaw in it, which is why the ground is not filled
   corner to corner.

   Not to be confused with the halo this file already refuses, a few rules
   down: that one was the low-resolution stand-in leaking out from under the
   picture while the real one loaded -- an accident during loading, not a
   ground. This one is behind the picture and never touches its edge. */
.viewer .backdrop .wash {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(100%, calc(100dvh * var(--shape, 1.3333)));
  aspect-ratio: var(--shape, 4 / 3);
  object-fit: cover;
  translate: -50% -50%;
  /* How far the light reaches. Larger, and the glow carries further out but
     the colour beside the edge comes from further inside it; this is where the
     two stop fighting. */
  scale: 1.7;
  filter: blur(52px) saturate(1.35);
  /* How bright is decided per picture, from the render: it is how near that
     picture is to the middle, the same number that places and sizes it. So the
     light crosses over with the carousel instead of being a second event after
     it. A rule here would have to guess at a value the render already knows. */
}

/* The whole screen is the target area. The picture sits in the middle of it,
   and what is left over either side is ground -- a tap there puts the picture
   down, the same as letting go of it.

   touch-action: none, because every gesture here is one we handle: without it
   the browser claims the horizontal drag as a scroll and the swipe never
   arrives. */
.viewer .stage {
  position: absolute;
  inset: 0;
  /* One track, and its size is stated. Left to `auto` the track takes the
     width of what is in it, and what is in it is sized from the track -- so a
     landscape photograph asked for 1122 pixels on a 390 pixel screen and got
     them. The same trap as the toolbar: a grid track is at least as wide as
     its widest item unless it is told what it is. */
  display: grid;
  grid-template: 100% / 100%;
  place-items: center;
  overflow: hidden;
  touch-action: none;
  /* The slides number themselves -- the middle one is 2, the two standing
     aside are 1 -- and without this those numbers were being read against
     everything else in the viewer rather than against each other. The arrows
     carry a 1 and the middle slide a 2, so over a picture wide enough to reach
     them the arrows were behind it: visible, and not pressable. Isolating the
     stage keeps the carousel's numbering to itself. */
  isolation: isolate;
}

/* Cut to the picture's own proportions, which is what makes the dark either
   side of it ground rather than part of the photograph.

   The width is taken first and the height only caps it, so a picture runs edge
   to edge unless it is more elongated than the screen -- which on a telephone
   held upright means every photograph a telephone takes, landscape or upright,
   fills the width. Nothing is ever cropped to do it, and nothing ever asks for
   the telephone to be turned. */
.viewer .frame {
  position: relative;
  margin: 0;
  /* Fitted to whichever edge it reaches first, said in one value.
     `width: 100%` with `max-height` looks like it says the same thing and does
     not: a stated width wins, the cap then squashes the height, and the box
     stops having the picture's proportions -- on a desk-sized window the frame
     was the whole window with the photograph letterboxed inside it. A ratio
     only ever derives the side that is left open.

     The height here is the dynamic one, so a telephone that hides its address
     bar as you scroll does not leave the picture overflowing the screen it was
     measured against. */
  width: min(100%, calc(100dvh * var(--shape, 1.3333)));
  aspect-ratio: var(--shape, 4 / 3);
  /* Clipped to the picture's own edges. The stand-in underneath is blurred and
     scaled a little past them, and without this its colours glowed out over
     the ground -- a halo around every photograph, which is not a photograph
     arriving, it is a mistake. */
  overflow: hidden;
  /* All three in the one cell, so they stack in the middle and the transform
     is the only thing that says where each of them is. The stage states its
     track sizes for the reason above it; a second row or column here would
     undo that. */
  grid-area: 1 / 1;
  /* The transition is written per frame, from the render: it is off while a
     finger is on the glass and on while the track runs home, and a rule here
     would fight whichever of the two is right. */
  will-change: transform;
}

.viewer .frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* No drag-to-save ghost, and no text selection starting under a swipe. */
  -webkit-user-drag: none;
  user-select: none;
}

/* The small preview, drawn underneath while the large one is on its way. It is
   the picture that was just tapped, so it is already in the browser's cache
   and appears in the same frame as the tap -- blurred, because at a fortieth
   of the pixels it would otherwise look like a mistake rather than like a
   picture arriving. */
.viewer .frame .waiting {
  filter: blur(12px);
  transform: scale(1.04);
  /* Taken away once the real one is fully in, not while it is arriving: two
     half-transparent pictures over a dark ground is a dip in the middle of the
     fade. The delay is the length of that fade. */
  transition: opacity 120ms ease 200ms;
}

.viewer .frame.settled .waiting {
  opacity: 0;
}

.viewer .frame .full {
  opacity: 0;
  transition: opacity 200ms ease-out;
}

/* A film fills its frame the way a photograph does, and brings the browser's
   own controls with it.

   `object-fit: contain` rather than cover: the frame is already cut to the
   proportions the film reported, so the two agree -- but they do not while the
   film is still saying what shape it is, and a frame that crops the first
   second of every film to a guess is not worth the pixels it saves. */
.viewer .frame .film {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--dark);
}
/* The controls belong to the film, so a finger on them is not a swipe. The
   element stops those events itself; this is the other half -- without it the
   stage claims the gesture before the controls ever see it. */
.viewer .frame .film { touch-action: auto; }

.viewer .frame .full.here {
  opacity: 1;
}

/* The bar over the top, on a wash rather than a bar: a solid strip would cut
   the top off every landscape photograph. A tap on the picture puts it away. */
.viewer header {
  position: absolute;
  /* Over the stage, for the same reason as the arrows: a picture tall enough
     to reach the top edge must not take the close button with it. */
  z-index: 1;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2);
  padding-top: max(var(--s2), env(safe-area-inset-top));
  color: var(--raised);
  background: linear-gradient(to bottom, var(--veil), transparent);
  transition: opacity 160ms ease, transform 160ms ease;
}

.viewer header.gone {
  opacity: 0;
  transform: translateY(-100%);
  pointer-events: none;
}

.viewer header .quiet,
.viewer header .quiet:hover {
  color: var(--raised);
  background: none;
  border-color: transparent;
  box-shadow: none;
}

.viewer header .quiet:hover {
  background: var(--veil);
}

/* An anchor doing a button's job: the download is a link so that the browser
   saves it rather than us fetching it into memory first. */
.viewer header a.quiet {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  border-radius: var(--radius);
}

.viewer .who {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.viewer .who .name {
  display: flex;
  min-width: 0;
  font-size: var(--t-sm);
}

.viewer .who .stem {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.viewer .who .ext {
  flex: none;
}

.viewer .who .of {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: var(--t-xs);
  opacity: 0.75;
}

/* For a pointer. On a telephone the gesture is the swipe, and two arrows
   sitting on the photograph would be in the way of it. */
.viewer .step {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  /* Above the stage. Everything in the viewer is absolutely positioned inside
     one element, so without a number here it would be the order of two lines
     in the markup that decided what a person can press. */
  z-index: 1;
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--raised);
  /* The veil and nothing else. It was made darker and given a hairline of
     light for a while, on the argument that it disappears over a dark
     picture -- which it does, and quietly disappearing is what a control on
     top of a photograph is supposed to do. What was actually wrong was that
     the arrows were behind the picture; the disc was never the problem. */
  background: var(--veil);
  border-color: transparent;
  border-radius: 50%;
  box-shadow: none;
  transition: opacity 160ms ease;
}

.viewer .step:hover {
  color: var(--raised);
  background: var(--veil);
  border-color: transparent;
  box-shadow: none;
}

/* Clear of the edge rather than against it. At twelve pixels the disc was
   touching the window, which on a picture that reaches the same edge reads as
   part of the frame instead of as something to press. */
.viewer .step.back { left: var(--s5); }
.viewer .step.on { right: var(--s5); }

.viewer .step.gone {
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 700px) {
  .viewer .step { display: none; }
}

/* ---------- wide ---------- */

/* The first min-width rule in this file, and the reason it is the first is
   that everything above it was written for a telephone and then narrowed
   further. Here it is the other way round: a window this wide has room for
   the things the phone hides behind a press, so they stop being hidden.

   Nothing below restyles a component. The strip, the wayfinder and the search
   field are the same pieces they are at 390 -- they stand somewhere else. */
@media (min-width: 64rem) {
  /* Two columns over the whole height. The left one carries everything that
     is not the folder; the right one is nothing but the folder. */
  .shell {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    grid-template-rows: 100dvh;
  }

  .rail {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    min-width: 0;
    border-right: 1px solid var(--line-soft);
    background: var(--ground);
  }
  /* Scrolls on its own. A folder twenty levels down must not push the places
     off the bottom of a column that cannot move. */
  .rail-inner {
    display: grid;
    align-content: start;
    gap: var(--s2);
    padding: var(--s3) var(--s2) var(--s4);
    overflow-y: auto;
    min-height: 0;
  }
  /* Level with the symbols in the list below it, with air on both sides so it
     is neither part of the field above nor the first entry below.

     A step above the list under it and a step below the sign-in screen, which
     is where it stands: bigger than a row, because it is the one place on the
     page that says whose cloud this is, and smaller than the screen that has
     nothing else on it. */
  .rail .brand { font-size: var(--t-ml); }
  /* The mark is a control here, so it takes a control's shape: the full width
     of the column, the padding that keeps it level with the list below, and
     the same hover the places use. It says what it does by lighting up like
     they do, not by looking like a link. */
  .rail .brand-home {
    display: block;
    width: 100%;
    padding: var(--s3) var(--s2);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: inherit;
    text-align: left;
    cursor: pointer;
  }
  .rail .brand-home:hover { background: var(--hover); }
  /* Even on both sides, for the search field and nothing else. The band's
     line is deliberately lopsided -- its leading symbol wants more room than
     its trailing button -- and the field standing here has no leading symbol,
     so what was room became a list that did not line up with itself.

     Only the search field, because this once took every .naming in the column
     with it: the dialogues then set their fields two hundred and fifty
     thousandths of an inch to the left of their own heading and their own
     explanation, which is exactly the sort of half-alignment that reads as
     carelessness without anybody being able to say why. */
  .rail .naming.searching { padding-inline: var(--s2); }
  /* A step in per level, in the column and only there.
     
     The band on a telephone has none, deliberately: an indent there says what
     the order already says, and it says it with a fifth of the width -- four
     levels down the name had 92 of 390 pixels. The column is the same 390
     wide, but it holds nothing else and it is read rather than glanced at on
     the way past, so a small step buys the shape of the tree for a price the
     names can pay. Twelve pixels a level, four levels deep at most: beyond
     that the indent would be saying "deep" rather than "how deep". */
  .rail .places .place.deeper { padding-left: calc(var(--s2) + var(--depth, 1) * var(--s3)); }

  /* The chain and the places under one another, not beside: the column is a
     column. The same shape the band takes on a narrow phone, and for the same
     reason -- two columns of four words are two columns of ellipsis. */
  .rail .wayfinder {
    grid-template-columns: 1fr;
    padding: 0;
  }
  .rail .places.elsewhere {
    border-top: 1px solid var(--edge);
    padding-top: var(--s2);
  }
  /* The band belongs to the column now, so its contents line up with the
     column rather than with a content column that is somewhere else. */
  .rail .band-inner > *,
  .rail .status-line { max-width: none; }

  main { min-height: 0; overflow-y: auto; }

  /* One width for every key in the row. Each sized to its own word, the four
     of them were four different rectangles for one kind of thing; at this
     width there is room to let them be one.

     The pair stays centred. Pinning the symbol to the left end lined the four
     symbols up in a column, which was the point -- and at a hundred and sixty
     pixels it also left every word floating in a field of its own, which read
     as a button that had not been finished. At a hundred and forty the words
     nearly fill the key, and centred they sit in it rather than in front of
     it. What made the row restless was four different widths, and that is
     what the one width fixes. */
  .toolbar .actions button { min-width: 8.75rem; }

  /* The figures move to the right-hand end of the row. On a telephone they sit
     under the name because there is nowhere else; across eleven hundred pixels
     that leaves the name and its figures crowded against the left edge with a
     hand's breadth of nothing between them and the dots.

     The entry turns from a column into a row -- the same two things, side by
     side instead of stacked. No second element and no fourth grid track: what
     a name and its figures are to each other does not change with the window,
     only which way round they sit. */
  .row .entry {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s5);
  }
  .row .entry .name { min-width: 0; }
  .row .meta {
    flex: none;
    flex-wrap: nowrap;
    gap: 0 var(--s4);
  }
  /* One width for every figure, so they stand in columns down the list.
     
     Each field sized to its own content, the three of them were three ragged
     edges: a row of 1 B and one of 12,4 MB put their dates in different places
     and the eye read the difference as movement rather than as figures. Built
     from the right, because the row is: the date is the last thing in it, and
     the size before it, so those two line up whether or not the entry above
     has a running time in front of them. */
  .row .meta > span { width: 5.5rem; }

  /* Tiles keep their size instead of multiplying. auto-fill with a 7rem floor
     and no ceiling put nine 117-pixel squares across the content column, which
     is a contact sheet rather than a folder. */
  .listing.grid { grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); }

  /* A sheet is read, a list is scanned, and those are two measures. Without
     this the quota bar ran eleven hundred pixels to say "0 %". */
  .sheet { max-width: 44rem; }
}
