/*
  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 */
  /* Pressed and latched, one tone per key. Not one shared tone: a single
     value put the lesser key's engaged state a single lightness step from its
     resting state, which is no state at all. Each is 8.4 steps below its own
     face -- the distance white had to the old recess, which is the depth this
     interface already reads as "held down". */
  --key-down: #c2bdae;
  --key-quiet-down: #d1ccbf;

  /* 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;
  /* One step past ink, for the hover of the default button. Named rather than
     written as #000 in a rule, so nothing in this file sets a colour that is
     not in this block. */
  --ink-strong: #000;

  /* 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-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;

  /* 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); }
button:active {
  background: var(--key-down);
  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 cap 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 {
  background: var(--key-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 {
  background: var(--ink-strong);
  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); }

/* 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 {
  background: var(--key-quiet-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;
}
/* 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;
  grid-template-columns: 1fr 1fr;
  gap: var(--s2);
  padding: var(--s2) var(--s4) var(--s3);
  align-items: start;
}
/* 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; }
.sheet .soon {
  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);
  max-width: 34rem;
}
.sheet .soon svg { flex: none; margin-top: 1px; }

/* 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); }

/* Last on the page and behind a rule: the rarest thing here, and the most
   expensive to press by accident. */
.sheet .out {
  justify-self: stretch;
  justify-content: flex-start;
  border: none;
  border-top: 1px solid var(--line-soft);
  border-radius: 0;
  padding-top: var(--s4);
  margin-top: var(--s2);
  background: none;
  box-shadow: none;
  color: var(--danger);
  gap: var(--s3);
}
/* The rule above it is a separator, not an edge. button.danger:hover paints
   every border in the warning colour, which is right on a button that has one
   and wrong here -- it turned the line over the row red on the way past. */
.sheet .out:hover {
  background: none;
  color: var(--danger);
  box-shadow: none;
  border-color: transparent;
  border-top-color: var(--line-soft);
}
.sheet .out:active { transform: none; box-shadow: none; }

.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. */
.places { display: grid; align-content: start; padding: var(--s2); gap: 2px; }
.places .place {
  display: flex;
  justify-content: flex-start;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border: none;
  background: none;
  box-shadow: none;
  border-radius: var(--radius-sm);
  color: var(--ink);
  text-align: left;
}
.places .place .fill { flex: 1; min-width: 0; }

/* 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 and nothing else. The lighter fill this used to
   carry was there because an earlier recess fill sat a hundredth away from the
   strip and marked nothing -- so the row was lifted out instead, which said
   "raised" about the one thing already chosen. A groove says it without
   arguing: no fill, the row keeps the colour of the strip, and the two
   hairlines are the whole of the depth. */
.places .place.on,
.choices .choice.on { background: none; box-shadow: var(--down-soft); }

/* ---------- 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;
  transition:
    opacity 260ms ease 60ms,
    visibility 0s linear 380ms;
}
.status.open .status-inner { opacity: 1; visibility: visible; transition-delay: 60ms, 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;
  transition:
    opacity 260ms ease 60ms,
    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-delay: 60ms, 0s; }

/* 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); }
/* 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);
}

/* 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. */
.toolbar {
  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 three lines take the fill of the band they have opened -- the same
   thing the name used to say with a chevron, said once instead of twice. */
.where .burger[aria-expanded="true"] { background: var(--chrome); color: var(--ink); }
/* 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); }


.row .meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s3);
  font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
  font-size: var(--t-xs);
  color: var(--ink-faint);
}
.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; }
/* 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 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);
}
/* 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;
  /* 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;
}

/* 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;
  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%);
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--raised);
  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;
}

.viewer .step.back { left: var(--s3); }
.viewer .step.on { right: var(--s3); }

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

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