/* GLiTCHED ViSUALS portal.
 *
 * Deliberately NOT importing glitched.css. That stylesheet is 2,187 lines built around
 * one scrolling marketing page - rails, lightboxes, hero plates - and almost none of it
 * applies to a signed-in tool. Inheriting it would mean every future change to the
 * public site risks moving something in a client's gallery. So the tokens below are
 * copied, not linked, and the two stylesheets are free to drift.
 *
 * If a token changes on the public site and matters here, it gets changed here too. That
 * is a deliberate small cost, paid to keep the portal stable. */

:root{
  /* Dark-only, said once. The list a select pops open, the scrollbars and the date
     picker are drawn by the browser, not by this stylesheet. Without this it draws them
     in light mode, so our white option text lands on a white popup and vanishes. */
  color-scheme: dark;

  --void:    #050506;
  --ink-2:   #0b0b0d;
  --ink-3:   #131316;
  --bone:    #f4f4f6;
  --silver:  #c9ced8;
  --dim:     #8e9199;
  --dimmer:  #565961;
  /* Two colours, not seven. The public site's full prismatic smear belongs on a
     marketing page; a tool you look at all day wants one idea, so the portal keeps the
     cool end of that spread and drops the rest. Violet into blue, always on black. */
  --violet:  #7b3fe0;
  /* What the 3px accent stripe actually reads as. It is painted with --line-accent, a
     90deg gradient, but across three pixels the eye gets the average rather than either
     end - which is this. Stated as a colour so text can match the stripe exactly instead
     of running the same gradient through letters and landing somewhere else. 4.05:1 on
     the card behind it. */
  --accent-edge: #5565e8;
  --blue:    #2f8bf0;
  --holo-a:  #7b3fe0;        /* kept as names so nothing downstream has to change */
  --holo-b:  #2f8bf0;
  --holo: linear-gradient(135deg, #7b3fe0 0%, #2f8bf0 100%);
  --line-accent: linear-gradient(90deg, #7b3fe0 0%, #2f8bf0 100%);
  --line:    rgba(244,244,246,.13);
  --line-2:  rgba(244,244,246,.26);
  --bad:     #ff5b6e;
  --good:    #46e0a8;

  /* Glass. Three weights of the same idea: a fill you can see through, a slightly
     stronger one for things that sit on top, and the hairline that separates them.
     All built from --bone so a surface picks up whatever is behind it rather than
     being painted a flat grey. */
  --glass:     rgba(244,244,246,.045);
  --glass-2:   rgba(244,244,246,.075);
  --glass-3:   rgba(244,244,246,.11);
  --glass-line:rgba(244,244,246,.12);

  /* Blur is expensive, so it is reserved for the few surfaces that genuinely float:
     the bar, a dialog, the notification panel. */
  --blur: blur(16px) saturate(140%);

  /* Corners. One value for panels, a smaller one for controls - mixing radii is what
     makes an interface look assembled rather than designed. */
  --r:    12px;
  --r-sm: 8px;
}

*,*::before,*::after{ box-sizing:border-box; }

/* The hidden attribute is only display:none in the UA stylesheet, so any rule of ours
   that sets display - .gate{display:grid} below, for one - silently outranks it. That
   left the session gate covering a portal that had already loaded behind it. Stating it
   once here means hidden means hidden, whatever else an element is styled as. */
[hidden]{ display:none !important; }

html,body{ height:100%; }

body{
  margin:0;
  background:var(--void);
  color:var(--bone);
  font:400 15px/1.55 "Space Mono", ui-monospace, "SF Mono", Menlo, monospace;
  -webkit-font-smoothing:antialiased;
}

/* A single wash of colour behind everything, so the black is not flat. Fixed rather
   than scrolled: it should feel like the light in the room, not part of the page. */
body::before{
  content:"";
  position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(60vw 50vh at 18% 0%,  rgba(47,139,240,.10), transparent 70%),
    radial-gradient(50vw 44vh at 88% 96%, rgba(255,47,154,.08), transparent 70%);
}

a{ color:var(--silver); }
a:hover{ color:var(--bone); }

/* ------------------------------------------------------------------ auth screen */

.auth{
  position:relative; z-index:1;
  min-height:100dvh;
  display:grid; place-items:center;
  padding:clamp(20px,6vw,64px) 20px;
}

.auth-card{
  width:100%; max-width:420px;
  background:linear-gradient(180deg, rgba(22,22,27,.72), rgba(11,11,14,.72));
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  box-shadow:0 30px 70px rgba(0,0,0,.5), inset 0 1px 0 rgba(244,244,246,.07);
  padding:clamp(26px,5vw,38px);
  position:relative;
  overflow:hidden;
}

/* The one piece of colour on the page. Same accent rule the public site uses for every
   thin line that carries the brand, so the portal reads as the same studio. */
.auth-card::before{
  content:"";
  position:absolute; top:0; left:0; right:0; height:2px;
  background:var(--line-accent);
}

.auth-mark{
  display:block;
  width:min(190px, 62%);
  height:auto;
  margin:0 auto 10px;
  /* The wordmark is a dark-background asset already; this only lifts it off pure black. */
  filter:drop-shadow(0 0 18px rgba(201,206,216,.18));
}

.auth-sub{
  margin:0 0 26px;
  text-align:center;
  font-size:11px;
  letter-spacing:.22em;
  text-transform:uppercase;
  color:var(--dim);
}

/* Each flow (sign in, set password, reset) is a panel in the same card. Only one is
   ever un-hidden, which keeps the transitions free and the markup honest about state. */
.panel{ display:none; }
.panel.on{ display:block; }

.panel h1{
  margin:0 0 6px;
  font:400 clamp(21px,4.4vw,26px)/1.1 "Anton", Impact, sans-serif;
  letter-spacing:.02em;
  text-transform:uppercase;
}

.panel p.lede{
  margin:0 0 22px;
  color:var(--dim);
  font-size:13px;
  line-height:1.6;
}

/* The band's name on the invitation panel, learned from the server after the page has
   rendered. Brighter than the line below it, because it is the one word on screen that
   tells a client this link was meant for them and not forwarded by mistake. */
.panel p.who{
  margin:0 0 10px;
  color:var(--silver);
  font-size:14px;
}
/* Nothing to say - a legacy Supabase link, or a name we could not fetch. An empty
   paragraph still carries its margin, and the gap reads as a missing sentence. */
.panel p.who:empty{ display:none; }

label{
  display:block;
  margin:0 0 7px;
  font-size:10px;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--dim);
}

input[type=email],
input[type=password],
input[type=text]{
  width:100%;
  background:var(--void);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  color:var(--bone);
  font:inherit;
  font-size:14px;
  padding:12px 13px;
  transition:border-color .16s ease, box-shadow .16s ease;
}
input:focus{
  outline:none;
  border-color:var(--line-2);
  box-shadow:0 0 0 1px rgba(47,139,240,.35), 0 0 22px rgba(47,139,240,.12);
}
input:disabled{ opacity:.5; }

.field{ margin:0 0 16px; }

/* Shown under an input when that one field is wrong, rather than putting every error in
   the banner. A client who mistypes a password should not have to re-read the page. */
.field-note{
  margin:7px 0 0;
  font-size:11px;
  color:var(--bad);
  min-height:0;
}

button.go{
  width:100%;
  margin-top:4px;
  padding:13px 16px;
  border:0; border-radius:var(--r-sm);
  background:var(--bone);
  color:var(--void);
  font:700 11px/1 "Space Mono", monospace;
  letter-spacing:.2em;
  text-transform:uppercase;
  cursor:pointer;
  transition:transform .14s ease, box-shadow .2s ease, opacity .14s ease;
}
button.go:hover:not(:disabled){
  box-shadow:0 0 26px rgba(220,228,240,.30);
}
button.go:active:not(:disabled){ transform:translateY(1px); }
button.go:disabled{ opacity:.45; cursor:default; }

/* Busy state. The label swaps to a dim pulse instead of a spinner: one less asset, and
   it cannot be mistaken for the page still loading. */
button.go.busy{ color:var(--dimmer); }
@keyframes pulse{ 50%{ opacity:.45; } }
button.go.busy{ animation:pulse 1.1s ease-in-out infinite; }

.alt{
  margin:18px 0 0;
  text-align:center;
  font-size:12px;
  color:var(--dimmer);
}
.alt button{
  background:none; border:0; padding:0;
  color:var(--silver);
  font:inherit;
  text-decoration:underline;
  text-underline-offset:3px;
  cursor:pointer;
}
.alt button:hover{ color:var(--bone); }

/* ------------------------------------------------------------------ banner */

.banner{
  margin:0 0 20px;
  padding:11px 13px;
  border:1px solid var(--line);
  border-left:2px solid var(--dim);
  border-radius:var(--r-sm);
  font-size:12.5px;
  line-height:1.5;
  color:var(--silver);
  display:none;
}
.banner.on{ display:block; }
.banner.bad{ border-left-color:var(--bad); color:#ffc6cd; }
.banner.good{ border-left-color:var(--good); color:#b9f2da; }

/* ------------------------------------------------------------------ foot */

.auth-foot{
  margin:22px auto 0;
  text-align:center;
  font-size:10px;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--dimmer);
}
.auth-foot a{ color:var(--dimmer); text-decoration:none; }
.auth-foot a:hover{ color:var(--silver); }

/* Password requirements, shown only while the field has focus or content, so the form
   is not shouting rules at someone who has not started typing yet. */
.rules{
  margin:9px 0 0; padding:0;
  list-style:none;
  font-size:11px;
  color:var(--dimmer);
}
.rules li{ padding-left:15px; position:relative; }
.rules li::before{
  content:"\2022";
  position:absolute; left:3px;
  color:var(--dimmer);
}
.rules li.met{ color:var(--good); }
.rules li.met::before{ content:"\2713"; left:1px; }

/* ------------------------------------------------------------------ signed-in shell */

/* The gate covers the page until the session is confirmed. It is markup rather than a
   class on body so there is no frame in which a logged-out visitor sees portal chrome. */
.gate{
  position:relative; z-index:1;
  min-height:100dvh;
  display:grid; place-content:center; justify-items:center;
  gap:10px;
  padding:40px 20px;
  text-align:center;
}
.gate-note{
  margin:0;
  font-size:11px;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--dim);
  animation:pulse 1.4s ease-in-out infinite;
}
.gate-note.bad{ color:#ffc6cd; animation:none; letter-spacing:.06em; text-transform:none; font-size:13px; }
.gate-note a{ color:var(--silver); }

.shell #app{ position:relative; z-index:1; }

.bar{
  display:flex; align-items:center; gap:clamp(14px,3vw,32px);
  padding:0 clamp(16px,3.5vw,32px);
  height:62px;
  border-bottom:1px solid var(--line);
  background:rgba(10,10,13,.6);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  border-bottom:1px solid var(--glass-line);
  position:sticky; top:0; z-index:20;
}

.bar-mark{ display:flex; align-items:center; flex:0 0 auto; }
.bar-mark img{ height:19px; width:auto; display:block; }

.bar-nav{ display:flex; gap:clamp(12px,2.4vw,24px); flex:1 1 auto; }
.bar-nav a{
  font-size:10px; letter-spacing:.2em; text-transform:uppercase;
  color:var(--dim); text-decoration:none;
  padding:6px 0;
  border-bottom:1px solid transparent;
}
.bar-nav a:hover{ color:var(--bone); }
.bar-nav a.on{ color:var(--bone); border-bottom-color:var(--holo-b); }

.bar-who{
  display:flex; align-items:center; gap:12px;
  margin-left:auto; flex:0 0 auto;
}
.who-name{
  font-size:12px; color:var(--silver);
  max-width:28ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.who-out{
  background:none; border:1px solid var(--line); border-radius:var(--r-sm);
  color:var(--dim);
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  padding:8px 10px; cursor:pointer;
  transition:color .14s ease, border-color .14s ease;
}
.who-out:hover:not(:disabled){ color:var(--bone); border-color:var(--line-2); }
.who-out:disabled{ opacity:.4; cursor:default; }

.page{
  padding:clamp(28px,5vw,56px) clamp(16px,3.5vw,32px) 80px;
  /* The header photograph hangs off this rather than off .head, so it can start at the
     bar instead of below the back link. */
  position:relative;
}

/* Layout gets the room, prose does not. 1240px still stopped the page well short of the
   edge the header bar runs to, which read as a wide empty margin down the right of every
   screen rather than as a measure. Layout now ends where the page padding ends, so the
   controls in a head row line up with the ones in the bar above them. A paragraph still
   wraps narrow below, because a line of text this wide is genuinely hard to read. */
.page > *{ max-width:none; }
.page > p{ max-width:760px; }

.loading{
  color:var(--dim); font-size:11px; letter-spacing:.2em; text-transform:uppercase;
  animation:pulse 1.4s ease-in-out infinite;
}

/* A back link that reads as a step up rather than a button. The arrow is in the rule
   so no view has to remember to type it. */
.back{
  display:inline-block;
  position:relative; z-index:2;
  margin:0 0 16px;
  font-size:12px; letter-spacing:.2em; text-transform:uppercase;
  color:var(--dim); text-decoration:none;
}
.back::before{ content:"\2190\00a0\00a0"; }
.back:hover{ color:var(--silver); }

.head{
  display:flex; align-items:flex-end; gap:20px; flex-wrap:wrap;
  margin:0 0 6px;
}
/* Everything in the header sits above the photograph behind it. :not() rather than a
   blanket rule, because the blanket one would give the banner itself position:relative
   and drop it back into the flow. */
.head > :not(.head-banner){ position:relative; z-index:1; }

/* The frame a shoot is remembered by, behind its own header rather than beside it. It
   runs past the page gutters to the edge of the window - the same negative inset as the
   page padding, so it stays flush as that padding clamps down on a phone - and it does
   not end on a line anywhere. Two gradients do that: one down into the page background,
   one in from both sides, so there is no edge to notice, only a photograph that stops
   being a photograph. */
.head-banner{
  position:absolute;
  z-index:0;
  /* Flush to .page, which already spans the whole window - its gutters are padding on
     the inside, not margin. Insetting by a negative gutter pushed it 32px past the right
     edge and gave the entire portal a horizontal scrollbar. Nothing negative is needed:
     0,0,0 is edge to edge, and the top lands directly under the bar.

     A fixed height rather than one measured from the header, because the header is a
     different height on a client, a project and a folder, and the photograph should
     not be. */
  top:0;
  left:0;
  right:0;
  height:clamp(260px, 36vh, 430px);
  overflow:hidden;
  pointer-events:none;
  opacity:0;
  transition:opacity .45s ease;
}
.head-banner.on{ opacity:1; }

/* The delivered frames, to choose a header from, inside the dialog. A strip rather than
   a grid: the pictures are the point, and scrolling sideways through a shoot is closer
   to how you would pick one off a contact sheet than paging a second modal would be. */
.strip{
  display:flex; gap:8px;
  margin:10px 0 0;
  padding:8px;
  overflow-x:auto; overflow-y:hidden;
  scrollbar-width:thin;
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:rgba(244,244,246,.025);
}
.strip-note{
  margin:4px 6px; font-size:12px; color:var(--dim);
}
.strip-pick{
  flex:none;
  width:74px; height:74px;
  padding:0; margin:0;
  cursor:pointer;
  overflow:hidden;
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:none;
  transition:border-color .12s ease, transform .12s ease;
}
.strip-pick img{ width:100%; height:100%; object-fit:cover; display:block; }
.strip-pick:hover{ border-color:var(--bone); transform:translateY(-1px); }
.strip-pick:focus-visible{ outline:1px solid var(--bone); outline-offset:2px; }
.strip-pick.is-on{
  border-color:var(--good);
  box-shadow:0 0 0 1px var(--good);
}
.head-banner img{
  width:100%; height:100%;
  display:block;
  object-fit:cover;
  /* Faces and horizons sit high in a live frame far more often than dead centre. */
  object-position:center 34%;
}
.head-banner::after{
  content:"";
  position:absolute; inset:0;
  background:
    linear-gradient(to bottom,
      rgba(5,5,6,.30) 0%, rgba(5,5,6,.58) 42%, rgba(5,5,6,.88) 78%, var(--void) 100%),
    linear-gradient(to right,
      var(--void) 0%, rgba(5,5,6,0) 20%, rgba(5,5,6,0) 80%, var(--void) 100%);
}

/* A pale photograph - a white backdrop, a snow shot, a bleached press image - takes the
   opposite scrim, and the words go dark over it. Darkening a white frame until white
   text sits on it comfortably means hiding most of the photograph, which is a poor
   trade for a picture somebody chose. It still has to reach the page background by the
   bottom, so the last fifth falls away to it. */
.head-banner.is-light::after{
  background:
    linear-gradient(to bottom,
      rgba(242,242,246,.34) 0%,
      rgba(242,242,246,.70) 40%,
      rgba(242,242,246,.68) 64%,
      rgba(70,70,78,.72) 88%,
      var(--void) 100%),
    linear-gradient(to right,
      rgba(242,242,246,.55) 0%, rgba(242,242,246,0) 22%,
      rgba(242,242,246,0) 80%, rgba(5,5,6,.35) 100%);
}

/* The words themselves. Only ever set while a light banner is actually behind them -
   paintBanners clears this on every route - so nothing here can leak onto a page whose
   header is the ordinary black. */
.page.on-light .back,
.page.on-light .head-text h1,
.page.on-light .head-text .sub,
.page.on-light .head-owner,
.page.on-light .head-text .sub a{
  color:var(--void);
}
.page.on-light .sub-over .tag{
  color:#16161a;
  border-color:rgba(5,5,6,.3);
  background:rgba(255,255,255,.35);
}
.page.on-light .sub-over .tag.is-active{
  color:#07432c;
  border-color:rgba(7,67,44,.35);
}
/* The amber summary line is tuned for a near black page and goes to mud on a pale one.
   A darker amber keeps it the same piece of information rather than making it another
   line of black text. */
.page.on-light .sub-sum,
.page.on-light .sub-sum em,
.page.on-light .sub-sum strong{ color:#7a4a00; }

/* The pen is drawn, not written, so it takes the colour the same way. */
.page.on-light .head-text .pen{ color:var(--void); opacity:.75; }
.page.on-light .head-text .pen:hover{ opacity:1; }

/* The one button sitting on the photograph needs the same treatment or it disappears
   into it. */
.page.on-light .head-acts .act{
  color:#f4f4f6;
  background:rgba(12,12,15,.86);
  border-color:rgba(12,12,15,.86);
}

/* Shorter where the header is already most of the screen. */
@media (max-width:640px){
  .head-banner{ height:clamp(200px, 30vh, 280px); }
}

/* A decorative photograph is exactly the thing to drop when somebody has asked for less
   movement and less noise. */
@media (prefers-reduced-motion:reduce){
  .head-banner{ transition:none; }
}
.head h1{
  margin:0; flex:1 1 auto;
  font:400 clamp(26px,5vw,38px)/1.05 "Anton", Impact, sans-serif;
  letter-spacing:.01em; text-transform:uppercase;
}

/* The one affordance per screen that creates something. Deliberately quiet - it sits
   beside the title rather than shouting from a corner. */
.act{
  flex:0 0 auto;
  background:var(--bone); color:var(--void);
  border:0; border-radius:var(--r-sm);
  font:700 10px/1 "Space Mono", monospace;
  letter-spacing:.18em; text-transform:uppercase;
  padding:11px 15px; cursor:pointer;
  transition:box-shadow .2s ease, transform .14s ease;
}
.act:hover{ box-shadow:0 0 24px rgba(220,228,240,.28); }
.act:active{ transform:translateY(1px); }

.sub{
  margin:0 0 26px;
  color:var(--dim); font-size:12.5px;
  display:flex; gap:14px; flex-wrap:wrap; align-items:baseline;
}

/* Secondary verbs - rename, archive, delete. Text, because they are not what the screen
   is for, and a row of buttons would compete with the work. */
.linky{
  background:none; border:0; padding:0;
  color:var(--silver); font:inherit;
  text-decoration:underline; text-underline-offset:3px;
  cursor:pointer;
}
.linky:hover{ color:var(--bone); }
.linky.danger{ color:var(--dimmer); }
.linky.danger:hover{ color:var(--bad); }

/* Stop, on a phone. A text link is the right weight for it - stopping a link should not
   look like the thing you came here to do - but 31px of underlined text is a poor target
   on glass. Padded out to 44 without changing how it reads. */
@media (hover:none){
  .quick .who-acts .linky{
    display:inline-flex; align-items:center;
    min-height:44px; padding:0 4px;
  }
}

.empty{
  margin:26px 0 0;
  padding:26px 0;
  border-top:1px solid var(--line);
  color:var(--dim); font-size:13px; line-height:1.65;
}
.aside{
  margin:16px 0 0;
  color:var(--dimmer); font-size:12px; line-height:1.6;
}

/* ------------------------------------------------------------------ rows */

.rows{ margin:22px 0 0; padding:0; list-style:none; border-top:1px solid var(--line); }
.rows li{ border-bottom:1px solid var(--line); }

.rows a,
.rows li > .row-main{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  grid-template-areas:"main meta" "sub meta";
  gap:3px 18px;
  align-items:center;
  padding:18px 2px;
  text-decoration:none;
  color:inherit;
  transition:padding-left .16s ease, background-color .16s ease;
}
/* A whole row of plain text (an asset that is not a link) still has to lay out, so the
   grid lives on the row itself in that case. */
.rows li:not(:has(a)){
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  grid-template-areas:"main meta" "sub meta";
  gap:3px 18px; align-items:center; padding:18px 2px;
}
.rows a:hover{ background:rgba(244,244,246,.03); padding-left:10px; }

.row-main{
  grid-area:main;
  display:flex; align-items:center; min-width:0;
  font-size:15.5px; color:var(--bone);
}
/* The name inside a caption or a row. It does the truncating, so the three dots fall at
   the end of the NAME and the state mark beside it survives - an ellipsis that ate the
   tick made an activated account read as one nobody had opened. */
.nm-txt{
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  min-width:0;
}
/* Whitespace between flex items is dropped, so the online dot sets its own gap. */
.row-main > .dot{ flex:0 0 auto; margin-left:7px; }
.row-sub{
  grid-area:sub;
  font-size:12.5px; color:var(--dim);
  overflow:hidden; white-space:nowrap;
  /* Faded rather than cut with three dots, the same way the note line is. A client's
     description can land here, and an ellipsis in the middle of a sentence reads as a
     truncation error where a fade reads as "there is more". Short text is untouched:
     the mask only ever covers the last stretch of the line, which is empty anyway. */
  -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 64px), transparent 100%);
  mask-image:linear-gradient(to right, #000 calc(100% - 64px), transparent 100%);
}
.row-meta{
  grid-area:meta;
  font-size:11px; letter-spacing:.12em; text-transform:uppercase;
  color:var(--dimmer); text-align:right; white-space:nowrap;
}

/* The link covers the picture and the words. Anything you can click separately -
   a status dropdown - sits out here beside it, not nested inside an anchor. */
.rows li:has(.row-link),
.rows li:has(.row-side){
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  align-items:center;
  gap:18px;
}
.row-link{ min-width:0; }
.row-side{
  display:flex; align-items:center; gap:14px;
  flex:0 0 auto;
  padding-right:2px;
}
.row-side .row-meta{ text-align:right; }

/* Archived: present, legible, obviously not live. */
.rows li.is-off .row-main{ color:var(--dim); }
.rows li.is-off .row-meta{ opacity:.6; }

/* Row actions sit in the meta column. Normal case is text, so they stay out of the way
   until wanted. */
.row-meta .linky{
  margin-left:14px;
  font-size:10px; letter-spacing:.14em; text-transform:uppercase;
  text-decoration:none;
  border-bottom:1px solid var(--line-2);
  padding-bottom:2px;
}
.row-meta .linky:hover{ border-bottom-color:currentColor; }

/* Against a 64px thumbnail, actions centred in the row float in the middle of nothing.
   Pinned to the top corner they line up with the title they belong to. */
#doc-list .row-meta{ align-self:start; padding-top:3px; }
#doc-list .row-meta .linky{ margin-left:0; }

/* The bin, drawn the same way as the pen so the pair match.

   .pen.bin-row rather than .bin-row: this block sits ABOVE the .pen rules in this file,
   and at equal specificity the later rule wins. Written as a single class it lost the
   width, the margin AND the --pen mask, so the bin came out as a 5.5px pencil sitting
   next to the real one. Two classes outranks one wherever either happens to be. */
.pen.bin-row{
  --pen:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6l1 2h4v2H4V5h4l1-2zM6 9h12l-1 11a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L6 9zm3 2v9h2v-9H9zm4 0v9h2v-9h-2z'/%3E%3C/svg%3E");
  width:16px; height:16px;
  margin:0 0 0 12px;
  -webkit-mask:var(--pen) center/contain no-repeat;
  mask:var(--pen) center/contain no-repeat;
  vertical-align:middle;
}
.pen.bin-row:hover{ background:var(--bad); }

/* ------------------------------------------------------------------ avatars */

/* A band is a picture before it is a word. Where there is no picture, initials on the
   brand gradient - which reads as a decision rather than a missing image. */
/* 4:5 throughout. Portrait is the shape most of this work actually is - a singer, a
   poster, a press shot - so the frame stops fighting the photograph, and one ratio
   everywhere means a row of them lines up instead of stepping. */
.av{
  grid-area:av;
  width:64px; height:80px;
  border-radius:var(--r-sm);
  object-fit:cover;
  display:grid; place-items:center;
  flex:0 0 auto;
}
.av-ph{
  background:var(--holo);
  color:var(--bone);
  font:700 12px/1 "Space Mono", monospace;
  letter-spacing:.04em;
}
/* The header picture is the one that should carry the page. At 84px it was a list
   thumbnail that happened to be at the top; at 160 it is the band. */
.av-big{ width:160px; height:200px; border-radius:var(--r); }
.av-big.av-ph{ font-size:46px; }

/* A row carrying an avatar needs a column for it. :has keeps the plain rows untouched. */
.rows a:has(.av),
/* ...and the same for a row that is not a link. The documents shelf is exactly that:
   its row has no anchor, because View and the bin are buttons rather than navigation.
   Without this the thumbnail had no column of its own and landed in the text one, which
   is why it sat out of line with everything above it. */
.rows li:not(:has(a)):has(.av){
  grid-template-columns:auto minmax(0,1fr) auto;
  grid-template-areas:"av main meta" "av sub meta";
  gap:2px 16px;
}

/* The third row exists only where there is something to put in it. Declaring the area
   unconditionally would have given every row in the portal - assets, folders, documents
   - an empty grid row and a few pixels of height for nothing. */
.rows a:has(.row-note){ grid-template-areas:"main meta" "sub meta" "note meta"; }
.rows a:has(.av):has(.row-note){
  grid-template-areas:"av main meta" "av sub meta" "av note meta";
}

/* What the shoot actually was, under the file count. One line: this is a list you scan,
   and a paragraph in every row turns it back into reading. It is cut by a mask rather
   than an ellipsis, so a sentence that runs out of room dissolves instead of stopping on
   three dots - and a short one is untouched, because the fade only ever covers the last
   stretch of the line, which for short text is empty anyway. */
.row-note,
.card-note{
  grid-area:note;
  font-size:12px; line-height:1.5;
  color:var(--dimmer);
  white-space:nowrap; overflow:hidden;
  -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 64px), transparent 100%);
  mask-image:linear-gradient(to right, #000 calc(100% - 64px), transparent 100%);
}

/* A card has room for two lines and no meta column beside them. */
.card-note{
  grid-area:auto;
  white-space:normal;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2;
  max-height:3em;
  -webkit-mask-image:linear-gradient(to bottom, #000 1.5em, rgba(0,0,0,.15) 3em);
  mask-image:linear-gradient(to bottom, #000 1.5em, rgba(0,0,0,.15) 3em);
}

.head-client{ align-items:center; gap:26px; margin-bottom:10px; }
.head-client .head-text{ flex:1 1 auto; min-width:0; }
.head-client .head-text h1{ font-size:clamp(32px, 6vw, 52px); }
.head-client .head-text .sub{ margin:10px 0 0; gap:10px; }

@media (max-width:560px){
  .head-client{ gap:16px; }
  .av-big{ width:104px; height:130px; }
  .av-big.av-ph{ font-size:30px; }
}

/* Pushed to the far right of the header, whatever the title does. Without margin-left
   the actions sit against the text and the Upload button lands mid-page. */
/* Two rows, right-aligned. Downloading a whole delivery is the thing a client comes
   here to do and the thing the studio does most often, so it sits on its own line as a
   button rather than as a word tucked between New folder and Upload. */
.head-acts{
  display:flex; flex-direction:column; align-items:flex-end; gap:11px;
  flex:0 0 auto; margin-left:auto;
}
.head-acts-row{ display:flex; align-items:center; gap:12px; }

/* A square the same height as Upload, so the row sits on one line. New folder is a
   thing you do occasionally and a word for it was as wide as the button that matters. */
.icon-act{
  width:34px; height:34px; flex:none;
  display:grid; place-items:center;
  padding:0; cursor:pointer;
  color:var(--silver);
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:transparent;
  transition:color .14s ease, border-color .14s ease, background-color .14s ease;
}
.icon-act svg{ width:17px; height:17px; display:block; }
.icon-act:hover{ color:var(--void); background:var(--bone); border-color:var(--bone); }
.icon-act:focus-visible{ outline:1px solid var(--bone); outline-offset:2px; }

/* The same shape as Upload without the weight. Upload is the one that creates
   something; this one only asks for what is already there. */
.act-quiet{
  background:transparent; color:var(--bone);
  border:1px solid var(--line-2);
  padding:11px 17px;
}
.act-quiet:hover{
  background:var(--bone); color:var(--void);
  border-color:var(--bone);
  box-shadow:0 0 24px rgba(220,228,240,.22);
}

/* Whether the account is live, and when they last looked, read before the name rather
   than trailing after their email address - they are the two things being checked. */
.sub-over{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  /* Sits between the back link and the header now, so it needs its own room above as
     well as below rather than only the gap the header gave it. */
  margin:-4px 0 16px;
  position:relative; z-index:2;
}

/* Access and attention, said plainly. A client with no login is normal, not an error. */
.tag{
  display:inline-flex; align-items:center; gap:6px;
  font-size:9px; letter-spacing:.16em; text-transform:uppercase;
  padding:3px 7px; border-radius:var(--r-sm);
  border:1px solid var(--line);
  color:var(--dimmer);
  white-space:nowrap;
}
.tag-on{ color:var(--good); border-color:rgba(70,224,168,.35); }

/* Looking right now. The only thing on these screens that moves on its own, which is
   the point: it should catch your eye once and then be ignorable. */
.tag-live{ color:var(--good); border-color:rgba(70,224,168,.4); }
.tag-live i,
.dot{
  width:6px; height:6px; border-radius:50%;
  background:var(--good);
  box-shadow:0 0 0 0 rgba(70,224,168,.55);
  animation:beat 2.4s ease-out infinite;
  flex:0 0 auto;
}
.dot{ display:inline-block; margin-left:7px; vertical-align:middle; }
@keyframes beat{
  0%   { box-shadow:0 0 0 0 rgba(70,224,168,.5); }
  70%  { box-shadow:0 0 0 6px rgba(70,224,168,0); }
  100% { box-shadow:0 0 0 0 rgba(70,224,168,0); }
}
.rows li.is-here .row-sub{ color:var(--good); }

/* The pen. Masked SVG rather than an icon font or an <img>: it inherits colour, so it
   can sit quietly beside a heading and brighten on hover without a second asset. */
.pen{
  --pen:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E");
  display:inline-block;
  vertical-align:middle;
  /* A button does not inherit font-size, so without this the em below resolves against
     the browser's 13px default and the pen comes out 7px beside a 38px heading. */
  font-size:inherit;
  /* Sized against the heading, and a touch above the baseline so it reads as an
     affordance rather than punctuation. */
  width:0.5em; height:0.5em;
  margin:0 0 .12em .4em;
  border:0; padding:0;
  background:var(--dim);
  -webkit-mask:var(--pen) center/contain no-repeat;
  mask:var(--pen) center/contain no-repeat;
  cursor:pointer;
  transition:background-color .14s ease;
}
.pen:hover{ background:var(--bone); }

/* The pen when it sits in a list row rather than beside a heading: sized in px, because
   there is no large heading nearby to take an em from. */
.pen-row{
  width:16px; height:16px;
  margin:0 0 0 14px;
  -webkit-mask:var(--pen) center/contain no-repeat;
  mask:var(--pen) center/contain no-repeat;
  vertical-align:middle;
}
.pen:focus-visible{ outline:1px solid var(--line-2); outline-offset:3px; }

/* ------------------------------------------------------------------ filters */

.filters{
  display:flex; flex-wrap:wrap; gap:10px 26px;
  margin:24px 0 0;
  padding:0 0 18px;
  border-bottom:1px solid var(--line);
}
.chips{ display:flex; flex-wrap:wrap; gap:8px; }

.chip{
  background:none;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  color:var(--dim);
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  padding:8px 11px;
  cursor:pointer;
  transition:color .14s ease, border-color .14s ease, background-color .14s ease;
}
.chip em{ font-style:normal; color:var(--dimmer); margin-left:5px; }
.chip:hover{ color:var(--bone); border-color:var(--line-2); }
.chip.on{ background:var(--bone); border-color:var(--bone); color:var(--void); font-weight:700; }
.chip.on em{ color:var(--dimmer); }

/* ------------------------------------------------------------------ bulk bar */

/* Appears only when something is ticked, and sticks to the top so it stays reachable
   halfway down a shoot of four hundred frames. */
/* The bar that appears once something is ticked. It used to be a row of underlined text
   links at one weight, which made eight equally shouty options with no sense of which
   was the usual one and which was the one you cannot undo. Now: a count, the action you
   almost always want, the rest as quiet pills, and the destructive one in red where it
   cannot be mistaken for the others. */
.bulk{
  position:sticky; top:0; z-index:12;
  display:flex; align-items:center; flex-wrap:wrap;
  gap:8px;
  margin:0; padding:12px 2px;
  background:rgba(5,5,6,.92);
  -webkit-backdrop-filter:blur(8px);
  backdrop-filter:blur(8px);
  border-bottom:1px solid var(--line);
}

.bulk-n{
  flex:none;
  margin-right:6px;
  padding:7px 12px;
  border-radius:999px;
  background:var(--bone);
  color:var(--void);
  font:700 11px/1 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
}

/* The bar as it looks the moment select mode opens, before anything has been ticked.
   The count is a solid white pill when it has a number to report; with nothing to report
   it would be a loud badge announcing an absence, which is the wrong emphasis for what
   is really a prompt to press the button beside it. */
.bulk.is-empty .bulk-n{
  background:none;
  color:var(--dim);
  box-shadow:inset 0 0 0 1px var(--line);
}

.bulk-act{
  flex:none;
  padding:7px 13px;
  border:1px solid var(--line-2);
  border-radius:999px;
  background:none;
  color:var(--silver);
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.12em; text-transform:uppercase;
  cursor:pointer;
  transition:color .14s ease, border-color .14s ease, background-color .14s ease;
}
.bulk-act:hover{ color:var(--bone); border-color:var(--bone); }

/* The one you reach for most of the time. */
.bulk-act.is-go{
  background:var(--bone); border-color:var(--bone); color:var(--void);
  font-weight:700;
}
.bulk-act.is-go:hover{ background:#fff; border-color:#fff; color:var(--void); }

.bulk-act.is-bad{ color:var(--bad); border-color:rgba(255,92,92,.38); }
.bulk-act.is-bad:hover{
  background:var(--bad); border-color:var(--bad); color:var(--void);
}

/* A hairline between the primary action and the rest. */
.bulk-sep{
  width:1px; height:18px;
  background:var(--line-2);
  margin:0 4px;
}

/* Housekeeping, pushed to the end and deliberately plain: these are not things you
   came here to do. */
.bulk-rest{ display:flex; align-items:center; gap:14px; margin-left:auto; }
.bulk-quiet{
  border:0; background:none; cursor:pointer;
  padding:4px 0;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--dimmer);
}
.bulk-quiet:hover{ color:var(--bone); }

@media (max-width:620px){
  .bulk-rest{ margin-left:0; }
  .bulk-sep{ display:none; }
}

/* A project's own words, above its files. Kept at reading measure even though the rest
   of the page now runs full width, and pre-line so a description typed as two paragraphs
   arrives as two paragraphs. */
.blurb{
  margin:18px 0 0;
  max-width:760px;
  font-size:14.5px; line-height:1.7;
  color:var(--silver);
  white-space:pre-line;
}

/* The multi-line field in a dialog. Matches the single-line inputs it sits beside. */
.field textarea{
  width:100%;
  padding:11px 13px;
  font:inherit; font-size:14px; line-height:1.6;
  color:var(--bone);
  background:var(--ink-3);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  resize:vertical;
  min-height:92px;
}
.field textarea:focus{ outline:none; border-color:var(--line-2); }

/* Where a client's login has got to. Four states, told apart by colour as well as by
   words, because down a list of twenty the colour is what you actually read. */
.tag-acc{ white-space:nowrap; }
.tag-acc.is-active{ color:#39d98a; border-color:rgba(57,217,138,.4); }
.tag-acc.is-sent{ color:#ff9a3c; border-color:rgba(255,154,60,.4); }
/* Opened but not finished. Between amber and green on purpose: something has happened
   since the email went out, and it is not nothing, but they are not in yet either. */
.tag-acc.is-opened{ color:#ffd166; border-color:rgba(255,209,102,.4); }
.tag-acc.is-none, .tag-acc.is-noemail{ color:var(--dimmer); }

/* The same three states as an icon. No border and no padding - a badge drawn round a
   16px glyph is mostly border, and these sit in rows that already have enough boxes. */
.acc-mark{
  display:inline-flex; align-items:center;
  width:16px; height:16px;
  flex:0 0 auto;
}
/* The last word of a name plus its mark. See withMark() - this is what stops the tick
   dropping to a line of its own on a narrow card. */
.nm-end{ white-space:nowrap; }
.acc-mark svg{ width:16px; height:16px; display:block; }
.acc-mark.is-active{ color:#39d98a; }
.acc-mark.is-sent{ color:#ff9a3c; }
.acc-mark.is-opened{ color:var(--blue); }

/* Against the name wherever a name is drawn. inline-flex with a baseline nudge rather
   than a flex child, because these names live in three different containers - a card
   caption, a row's main cell and a heading - and only one of them is a flex row. */
.card-name .acc-mark,
.row-main .acc-mark,
.who-text > strong .acc-mark,
h1 .acc-mark{
  display:inline-flex; vertical-align:-3px;
  margin-left:7px;
}
/* The page heading is set much larger than the lists, so a 16px glyph beside it reads
   as a speck. Scaled to the type rather than to a fixed size. */
h1 .acc-mark{ width:0.52em; height:0.52em; vertical-align:0.06em; }
h1 .acc-mark svg{ width:100%; height:100%; }

/* An entry the studio put on the calendar by hand. Told apart from a deadline by being
   filled rather than outlined: a due date is something owed, an entry is just a fact
   about the day, and they should not read as the same kind of thing. */
.cal-own{
  display:block; width:100%;
  text-align:left;
  border:0;
  background:var(--glass-3);
  color:var(--bone);
  cursor:pointer;
}
.cal-own:hover{ background:var(--bone); color:var(--void); }

/* The same row as a deadline in the Coming up list, but a button. */
.up-entry{
  display:flex; align-items:baseline; gap:14px;
  width:100%; padding:11px 14px;
  border:0; background:none; cursor:pointer;
  font:inherit; text-align:left;
}
.up-entry:hover{ background:var(--glass); }

/* Whose project this is, above its name. Deliberately small and above rather than
   beside: you are already looking at the project, this answers the question you ask a
   second later, and it is a way back to the artist as well as a label. */
.head-owner{
  display:inline-flex; align-items:center; gap:9px;
  margin:0 0 8px;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--silver);
  text-decoration:none;
}
.head-owner:hover{ color:var(--bone); }
.head-owner img, .head-owner-ph{
  width:22px; height:22px; flex:none;
  border-radius:50%;
  object-fit:cover;
  display:grid; place-items:center;
}
.head-owner-ph{
  background:var(--glass-3);
  font-size:9px; letter-spacing:.04em; color:var(--silver);
}

/* Arrived since this client last looked. Top right, where the studio's selection badge
   sits in the other mode - the two are never on screen together, because a client never
   selects and the studio never sees these. */
.tile-new{
  position:absolute; right:10px; top:10px;
  padding:4px 8px;
  font:700 9px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--void);
  background:var(--bone);
  border-radius:999px;
  pointer-events:none;
}

/* Picking whole clients or projects. The box sits over the thumbnail in both layouts
   rather than taking a column, so turning the mode on does not reflow the list. */
.rows li, .card{ position:relative; }
.row-pick{
  position:absolute; left:8px; top:8px; z-index:2;
  width:20px; height:20px;
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:rgba(5,5,6,.72);
  -webkit-backdrop-filter:blur(3px);
  backdrop-filter:blur(3px);
  cursor:pointer;
}
.row-pick:hover{ border-color:var(--bone); }
.rows li.is-picked .row-pick,
.card.is-picked .row-pick{ background:var(--bone); border-color:var(--bone); }
.rows li.is-picked .row-pick::before,
.card.is-picked .row-pick::before{
  content:"";
  position:absolute; left:6px; top:2px;
  width:5px; height:10px;
  border:solid var(--void); border-width:0 2px 2px 0;
  transform:rotate(45deg);
}
.rows li.is-picked{ background:var(--glass); }
.card.is-picked .card-shot{ box-shadow:0 0 0 2px var(--bone); }

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

.grid{
  margin:26px 0 0; padding:0;
  list-style:none;
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, var(--tile, 280px)), 1fr));
  gap:clamp(14px, 1.8vw, 24px);
}

.tile{ position:relative; min-width:0; }

.tile-shot{
  position:relative;
  width:100%; padding:0; margin:0;
  font:inherit; color:inherit; text-align:inherit;
  cursor:pointer;
  aspect-ratio:4 / 5;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--ink-3);
  display:grid; place-items:center;
  overflow:hidden;
  transition:border-color .16s ease;
}
.tile:hover .tile-shot{ border-color:var(--line-2); }
.tile-shot img{ width:100%; height:100%; object-fit:cover; display:block; }

/* Stand-in until the file store is connected. Faint diagonals so an empty tile reads as
   "nothing loaded here yet" rather than as a broken image. */
.tile-ph{
  font:700 11px/1 "Space Mono", monospace;
  letter-spacing:.26em;
  color:var(--dimmer);
  width:100%; height:100%;
  display:grid; place-items:center;
  background:repeating-linear-gradient(135deg,
    rgba(244,244,246,.022) 0 10px, transparent 10px 20px);
}

/* Video marker. Drawn rather than an asset, so it cannot 404 and costs no request. */
.tile-play{
  position:absolute; left:50%; top:50%;
  width:46px; height:46px; margin:-23px 0 0 -23px;
  border:1px solid var(--line-2);
  border-radius:50%;
  background:rgba(5,5,6,.45);
  backdrop-filter:blur(2px);
}
.tile-play::after{
  content:"";
  position:absolute; left:18px; top:14px;
  border-style:solid;
  border-width:9px 0 9px 14px;
  border-color:transparent transparent transparent var(--bone);
}

/* Status, on the image rather than under it: at a glance down a wall of forty frames
   you should be able to see which have gone out without reading a word. */
.tile-flag{
  position:absolute; left:0; top:0;
  padding:5px 9px;
  font:400 9px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--dim);
  background:rgba(5,5,6,.78);
  backdrop-filter:blur(3px);
  border-right:1px solid var(--line);
  border-bottom:1px solid var(--line);
}
.tile.is-brief .tile-shot{ border-style:dashed; }

/* Only drawn while selecting. The whole picture is the hit area now, so this is a badge
   saying what will happen rather than the target you have to find. */
.tile-pick{
  position:absolute; right:10px; top:10px;
  width:20px; height:20px;
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:rgba(5,5,6,.6);
  -webkit-backdrop-filter:blur(3px);
  backdrop-filter:blur(3px);
  transition:background-color .12s ease, border-color .12s ease;
  pointer-events:none;
}
.tile-shot:hover .tile-pick{ border-color:var(--bone); }
.tile.is-picked .tile-pick{
  background:var(--bone);
  border-color:var(--bone);
}
/* the tick, drawn so there is no icon font to load */
.tile.is-picked .tile-pick::before{
  content:"";
  position:absolute; left:6px; top:3px;
  width:5px; height:10px;
  border:solid var(--void);
  border-width:0 2px 2px 0;
  transform:rotate(45deg);
}
.tile.is-picked .tile-shot{
  border-color:var(--bone);
  box-shadow:0 0 0 2px var(--bone);
}

.tile-foot{
  display:flex; align-items:baseline; gap:10px;
  padding:10px 2px 0;
}
.tile-name{
  flex:1 1 auto; min-width:0;
  font-size:12.5px; color:var(--bone);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.tile-meta{
  flex:0 0 auto;
  font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--dimmer);
}

/* Actions stay out of the way until the tile is wanted. Always visible on touch, where
   there is no hover to reveal them. */
/* On the picture, not under it. A row of four words beneath every frame was four words
   to read forty times down a contact sheet, and it pushed the next row of images off the
   screen. These are marks in a corner of the image that is already being looked at. */
.tile-tools{
  position:absolute; right:8px; top:8px;
  display:flex; gap:6px;
  opacity:0;
  transform:translateY(-3px);
  transition:opacity .14s ease, transform .14s ease;
  pointer-events:none;
}
.tile:hover .tile-tools,
.tile:focus-within .tile-tools{
  opacity:1;
  transform:none;
  pointer-events:auto;
}
/* The dots, phone only. On a desktop the five tools appear on hover across the top of
   the tile and there is room for them; on a 375px screen two tiles across leaves about
   170px, which is not room for five 34px buttons - they ran off the edge of the tile
   and over the one beside it. */
.tile-dots{
  display:none;
  position:absolute; right:6px; top:6px;
  width:34px; height:34px;
  padding:0;
  border:1px solid var(--glass-line);
  border-radius:999px;
  background:rgba(18,18,22,.82);
  -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
  cursor:pointer;
  z-index:3;
}
/* Three dots, drawn with one element and two shadows. */
.tile-dots i{
  display:block;
  width:3px; height:3px;
  margin:0 auto;
  border-radius:50%;
  background:var(--bone);
  box-shadow:0 -6px 0 var(--bone), 0 6px 0 var(--bone);
}

@media (hover:none), (max-width:620px){
  .tile-dots{ display:block; }

  /* Shut until the dots are pressed, then a column under them rather than a row across
     the tile - a column has as much room as the tile is tall, which is plenty. */
  .tile .tile-tools{
    right:6px; top:46px;
    flex-direction:column;
    display:none;                 /* opened by showTools(), which sets display inline */
    opacity:1; pointer-events:auto;
    transform:none;
    transition:none;
    padding:6px;
    border:1px solid var(--glass-line);
    border-radius:12px;
    background:rgba(14,14,18,.94);
    -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
    box-shadow:0 18px 40px rgba(0,0,0,.5);
    z-index:4;
  }
  .tile.tools-on .tile-tools{
    opacity:1; pointer-events:auto; transform:none;
  }
  .tile.tools-on .tile-dots{ border-color:var(--line-2); }
}

/* Nothing hovers on a phone, so they used to simply stay - which is what put five
   buttons across a 170px tile. They now live behind the dots above, so this is gone
   rather than overridden: a rule that no longer does anything is a rule somebody will
   later spend an afternoon on. */

.tool{
  width:30px; height:30px;
  flex:none;
  display:grid; place-items:center;
  padding:0; margin:0;
  cursor:pointer;
  color:var(--silver);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:rgba(5,5,6,.72);
  -webkit-backdrop-filter:blur(4px);
  backdrop-filter:blur(4px);
  transition:color .12s ease, border-color .12s ease, background-color .12s ease;
}
.tool svg{ width:16px; height:16px; display:block; }
.tool:hover{
  color:var(--void);
  background:var(--bone);
  border-color:var(--bone);
}
.tool:focus-visible{ outline:1px solid var(--bone); outline-offset:2px; }

/* Already spoken for. Green at a glance, so the same frame is not picked twice down a
   wall of forty - the hover says which galleries, and whether it is built yet. */
.tool.is-on{
  color:var(--good);
  border-color:rgba(70,224,168,.45);
  background:rgba(70,224,168,.1);
}
.tool.is-on:hover{
  color:var(--void);
  background:var(--good);
  border-color:var(--good);
}

/* The one that cannot be undone is the one that looks different before it is pressed. */
.tool.is-bad:hover{
  color:var(--void);
  background:var(--bad, #ff6b6b);
  border-color:var(--bad, #ff6b6b);
}

/* A larger target where the pointer is a finger. */
@media (hover:none){
  .tool{ width:34px; height:34px; }
}

/* ---------------------------------------------------------- the homepage menu */

/* The house and its galleries are one hover area. A gap between the button and the
   menu would drop the hover halfway down, so the menu's own top padding bridges it
   and the wrapper covers both. */
/* Static on a tile so the menu hangs off the icon row's right edge rather than the
   house's - the house is the leftmost of the three, and a menu aligned to it alone
   spilled past the side of the picture. The hover still works from the house, because
   :hover follows the DOM and the menu is inside it. */
.tool-home{ position:static; display:inline-flex; }
.bulk-home{ position:relative; }

.home-menu{
  position:absolute; right:0; top:100%;
  z-index:30;
  min-width:172px;
  padding:7px 0 0;
  opacity:0;
  transform:translateY(-4px);
  pointer-events:none;
  transition:opacity .14s ease, transform .14s ease;
}
.tool-home:hover .home-menu,
.tool-home:focus-within .home-menu{
  opacity:1; transform:none; pointer-events:auto;
}

.home-menu > :not(.home-menu-t):first-of-type,
.home-menu-t{ border-top-left-radius:var(--r-sm); border-top-right-radius:var(--r-sm); }
.home-menu > :last-child{
  border-bottom-left-radius:var(--r-sm); border-bottom-right-radius:var(--r-sm);
}

.home-menu-t{
  display:block;
  padding:9px 12px 7px;
  font:400 8.5px/1.3 "Space Mono", ui-monospace, monospace;
  letter-spacing:.17em; text-transform:uppercase;
  color:var(--dimmer);
  background:rgba(5,5,6,.96);
  border:1px solid var(--line);
  border-bottom:0;
  -webkit-backdrop-filter:blur(6px);
  backdrop-filter:blur(6px);
}

.home-opt{
  display:block; width:100%;
  padding:8px 12px;
  text-align:left; cursor:pointer;
  font:400 11.5px/1 "Space Mono", ui-monospace, monospace;
  letter-spacing:.06em;
  color:var(--silver);
  background:rgba(5,5,6,.96);
  border:1px solid var(--line);
  border-bottom:0;
  -webkit-backdrop-filter:blur(6px);
  backdrop-filter:blur(6px);
  transition:color .1s ease, background-color .1s ease;
}
.home-menu > .home-opt:last-child{ border-bottom:1px solid var(--line); }
.home-opt:hover{ color:var(--void); background:var(--bone); }
.home-opt:focus-visible{ outline:1px solid var(--bone); outline-offset:-1px; }

/* Where the frame already is. Pressing one of these takes it off again, so the tick
   is also the affordance - and the red on hover says which way it is about to go. */
.home-opt.is-on::after,
.home-opt.is-some::after{
  content:"✓";
  float:right; margin-left:10px;
}
/* The same green as the house, so the icon and the gallery it refers to are obviously
   the same fact. A dash and a muted green for a selection where only some of the frames
   are in: pressing it adds the rest rather than taking the others off. */
.home-opt.is-on{
  color:var(--good);
  background:rgba(70,224,168,.09);
}
.home-opt.is-some{ color:rgba(70,224,168,.6); }
.home-opt.is-some::after{ content:"–"; }
.home-opt.is-on:hover,
.home-opt.is-some:hover{ color:var(--void); background:var(--bad, #ff5b6e); }

/* On the bulk bar the row runs left to right and there is room, so it drops straight
   down under the button instead of hugging a corner. */
.bulk-home .home-menu{ right:auto; left:0; top:100%; }


/* ------------------------------------------------------------------ dialog */

.ask{
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  background:rgba(18,18,22,.72);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 30px 70px rgba(0,0,0,.55), inset 0 1px 0 rgba(244,244,246,.06);
  color:var(--bone);
  padding:0;
  width:min(420px, calc(100vw - 32px));
}
.ask::backdrop{ background:rgba(2,2,4,.72); backdrop-filter:blur(2px); }
.ask form{ padding:26px; position:relative; }

/* Two strokes crossed, drawn rather than fetched. Sits in the padding so it never
   collides with the heading, however long the heading is. */
.ask-x{
  position:absolute; right:12px; top:12px;
  width:36px; height:36px;
  border:0; border-radius:var(--r-sm);
  background:none;
  cursor:pointer;
  transition:background-color .14s ease;
}
.ask-x::before,
.ask-x::after{
  content:"";
  position:absolute; left:50%; top:50%;
  width:13px; height:1px;
  margin:-1px 0 0 -7px;
  background:var(--dim);
  transition:background-color .14s ease;
}
.ask-x::before{ transform:rotate(45deg); }
.ask-x::after{ transform:rotate(-45deg); }
.ask-x:hover{ background:var(--glass-2); }
.ask-x:hover::before, .ask-x:hover::after{ background:var(--bone); }
.ask-x:focus-visible{ outline:1px solid var(--line-2); outline-offset:2px; }

/* Keep the title clear of it. */
.ask h2{ padding-right:34px; }
.ask h2{
  margin:0 0 8px;
  font:400 20px/1.15 "Anton", Impact, sans-serif;
  letter-spacing:.02em; text-transform:uppercase;
}
.ask-note{ margin:0 0 18px; color:var(--dim); font-size:12.5px; line-height:1.6; }
.ask label{ margin-bottom:7px; }
.ask .field:last-child{ margin-bottom:0; }

/* Per-field guidance, under the input it explains rather than stacked in the note. */
.field-hint{
  margin:6px 0 0;
  font-size:11px; line-height:1.5;
  color:var(--dimmer);
}

/* The destructive option, kept well away from Save. */
.ask-extra{
  margin:18px 0 0;
  padding-top:16px;
  border-top:1px solid var(--line);
  text-align:center;
}
.ask-extra .linky{ font-size:11px; }
.ask-row{ display:flex; gap:10px; margin-top:20px; }
.ask-row .go{ margin-top:0; flex:1 1 auto; width:auto; }
.ask-cancel{
  flex:0 0 auto;
  background:none; border:1px solid var(--line); border-radius:var(--r-sm);
  color:var(--dim);
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  padding:13px 14px; cursor:pointer;
}
.ask-cancel:hover{ color:var(--bone); border-color:var(--line-2); }
/* Destructive confirmation. Red, so the button does not look like every other Save. */
.go.danger{ background:var(--bad); color:#2a0208; }
.go.danger:hover{ box-shadow:0 0 26px rgba(255,91,110,.35); }

/* The bar used to wrap onto three rows here, to fit five navigation links, a name and
   a Sign out button onto a phone. All three of those have moved - the links to the tab
   bar along the bottom, the name and Sign out into the menu - so it is one row again.
   The rules that do it are in the phone block at the end of this file, kept together
   with the rest of the handset layout rather than left behind here. */



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

/* Four states, four weights of attention. On hold is the one that should catch your
   eye in a list of thirty; delivered is the one that should stop asking for it. */
.st{
  display:inline-block;
  font-size:11px; letter-spacing:.12em; text-transform:uppercase;
  padding:5px 9px; border-radius:var(--r-sm);
  border:1px solid var(--line);
  color:var(--dim);
  white-space:nowrap;
}

/* The same chip, but it is a control. Styled as the chip rather than as a browser
   dropdown so a list of them still reads as a column of statuses, not a form. */
.st-pick{
  -webkit-appearance:none; appearance:none;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.12em; text-transform:uppercase;
  padding:6px 24px 6px 9px;
  border-radius:var(--r-sm);
  border:1px solid var(--line);
  background:
    linear-gradient(45deg, transparent 50%, currentColor 50%) calc(100% - 12px) 50% / 4px 4px no-repeat,
    linear-gradient(135deg, currentColor 50%, transparent 50%) calc(100% - 8px) 50% / 4px 4px no-repeat,
    var(--void);
  color:var(--dim);
  cursor:pointer;
  transition:border-color .14s ease, color .14s ease;
}
.st-pick:hover{ border-color:var(--line-2); }
.st-pick:focus{ outline:none; border-color:var(--line-2);
                box-shadow:0 0 0 1px rgba(47,139,240,.3); }
.st-pick:disabled{ opacity:.5; cursor:default; }
.st-pick option{ background:var(--ink-3); color:var(--bone); letter-spacing:0; }

/* Deadlines, read at a glance. */
.tag-soon{ color:#ffcf6b; border-color:rgba(255,207,107,.4); }
.tag-late{ color:var(--bad); border-color:rgba(255,91,110,.45); }

/* The deadline tag takes its hue from how far off the date is - dueHue() in app.js
   writes it into --due-h. One rule covers the whole run from green to red, so there is
   no list of colour classes to keep in step with the thresholds. */
.tag-due{
  color:hsl(var(--due-h, 58) 90% 68%);
  border-color:hsl(var(--due-h, 58) 90% 68% / .42);
  background:hsl(var(--due-h, 58) 90% 68% / .07);
}
/* Overdue is the one that should stop you, so it gets weight the others do not. */
.tag-due.is-late{
  color:hsl(0 95% 72%);
  border-color:hsl(0 95% 72% / .5);
  background:hsl(0 95% 72% / .12);
  font-weight:700;
}

/* The deadline sits under the status rather than beside it: they answer two different
   questions, and on one line the eye reads them as a single long label. */
.row-state{
  display:flex; flex-direction:column; align-items:flex-end; gap:6px;
}
/* Four states, four colours, read as a traffic light: red has stopped, cyan has begun,
   orange is moving, green is done. Colour carries the meaning before the word does, so a
   column of thirty projects can be skimmed rather than read.
 *
 * Violet is deliberately not here. It already means "a client did this" in the activity
 * log, and one colour cannot mean two things. */
.st-hold{    --sc:#ff5b6e; }
.st-started{ --sc:#2ee6f0; }
.st-wip{     --sc:#ff9a3c; }
.st-done{    --sc:#46e0a8; }

.st-hold,  .st-started,  .st-wip,  .st-done{ color:var(--sc); }

/* A tinted fill on the static chip; the dropdown keeps its own background because that
   is what draws its arrow. */
.st.st-hold{    border-color:rgba(255,91,110,.48);  background:rgba(255,91,110,.09); }
.st.st-started{ border-color:rgba(46,230,240,.42);  background:rgba(46,230,240,.08); }
.st.st-wip{     border-color:rgba(255,154,60,.48);  background:rgba(255,154,60,.09); }
.st.st-done{    border-color:rgba(70,224,168,.45);  background:rgba(70,224,168,.08); }

.st-pick.st-hold{    border-color:rgba(255,91,110,.48); }
.st-pick.st-started{ border-color:rgba(46,230,240,.42); }
.st-pick.st-wip{     border-color:rgba(255,154,60,.48); }
.st-pick.st-done{    border-color:rgba(70,224,168,.45); }

.row-meta .st{ margin-right:10px; }

/* The project summary on a client. Coloured by the status it is counting, so a row of
   clients can be read for trouble before it is read for names - the one with something
   on hold is red whatever it is called. */
.sum{
  color:var(--sc, var(--dim));
  font-size:11px; letter-spacing:.1em; text-transform:uppercase;
  white-space:nowrap;
}
.sum-sep{ color:var(--dimmer); margin:0 2px; }
.sub-sum{ margin-top:8px; }

/* ------------------------------------------------------------------ calendar */

/* The list comes before the grid on purpose. A month view answers "when is that due";
   this answers "what do I have to do", which is the question people actually arrive
   with. The grid is for orientation afterwards. */
.upcoming{ margin:4px 0 34px; }
.up-head{
  margin:0 0 10px;
  font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:var(--dim);
}
.up-list{ list-style:none; margin:0; padding:0; }
.up-list li + li{ border-top:1px solid var(--line); }
.up-list a{
  display:grid; grid-template-columns:130px 1fr auto auto; gap:16px;
  align-items:baseline; padding:11px 10px; border-radius:var(--r-sm);
  color:var(--bone); text-decoration:none;
}
.up-list a:hover{ background:var(--glass); }
.up-when{
  font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--dim);
  white-space:nowrap;
}
.up-when.is-late{ color:var(--bad); }
.up-when.is-soon{ color:#ff9a3c; }
.up-name{ font-size:14px; }
.up-who, .up-date{ font-size:11px; color:var(--dimmer); white-space:nowrap; }

.cal-bar{ display:flex; align-items:center; gap:12px; margin:0 0 12px; }
.cal-title{
  margin:0; font-size:13px; letter-spacing:.14em; text-transform:uppercase;
  min-width:190px; text-align:center;
}
/* Pushed to the far end of the row, away from the two arrows. It does something
   different from them - it jumps rather than steps - and sitting beside them invited a
   stray click. */
.cal-today{ margin-left:auto; }

.cal-grid{
  display:grid; grid-template-columns:repeat(7, 1fr); gap:1px;
  background:var(--line); border:1px solid var(--line); border-radius:var(--r);
  overflow:hidden;
}
.cal-dow{
  background:var(--ink-2); padding:9px 10px;
  font-size:9px; letter-spacing:.16em; text-transform:uppercase; color:var(--dimmer);
}
.cal-cell{
  background:var(--void); min-height:96px; padding:7px 7px 9px;
  display:flex; flex-direction:column; gap:4px;
}
.cal-cell.is-empty{ background:var(--ink-2); opacity:.45; }
.cal-cell.is-today{ background:var(--glass-2); }
.cal-day{ font-size:11px; color:var(--dimmer); }
.cal-cell.is-today .cal-day{ color:var(--bone); font-weight:700; }

/* Coloured by the project's status, the same four colours used everywhere else, so the
   grid needs no legend of its own. */
.cal-pill{
  display:block; padding:3px 6px; border-radius:4px;
  font-size:10.5px; line-height:1.35; text-decoration:none;
  color:var(--bone); background:var(--glass-2);
  border-left:2px solid var(--sc, var(--dim));
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.cal-pill:hover{ background:var(--glass-3); }
.cal-pill.is-late{ border-left-color:var(--bad); }

/* Three filter groups on one line, wrapping to their own lines on a narrow screen. The
   little labels matter: without them three rows of chips is a puzzle rather than a tool. */
.act-filters{ flex-wrap:wrap; gap:10px 22px; align-items:center; }
.act-filters .chips{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.chips-label{
  font-size:9px; letter-spacing:.16em; text-transform:uppercase;
  color:var(--dimmer); margin-right:2px;
}
/* The select borrows the status picker's styling, minus the status colour. */
.act-thing{ color:var(--silver); padding:6px 26px 6px 10px; font-size:11px; }

.act-count{
  margin:14px 0 0;
  font-size:11px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--dimmer);
}
.act-count .linky{ font-size:11px; letter-spacing:.1em; }

/* A thumbnail arrives after its tile does, so it fades in rather than snapping. Starting
   at zero also hides the broken-image glyph when a link never arrives. */
.tile-shot img[data-shot]{ opacity:0; transition:opacity .25s ease; }
.tile-shot img[data-shot].on{ opacity:1; }

/* The upload panel. Bottom right, above everything, and deliberately not modal: a
   gallery filling with forty frames is something you watch while carrying on. */
.uq{
  position:fixed; right:18px; bottom:18px; z-index:70;
  transform:translateY(16px);
  opacity:0;
  transition:opacity .2s ease, transform .2s ease;
  width:min(380px, calc(100vw - 32px));
  background:rgba(18,18,22,.86);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  box-shadow:0 24px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(244,244,246,.06);
  overflow:hidden;
}
.uq.on{ opacity:1; transform:translateY(0); }

.uq-head{
  display:flex; align-items:center; gap:10px;
  padding:12px 12px 12px 16px;
  border-bottom:1px solid var(--line);
}
.uq-title{
  margin:0; flex:1; min-width:0;
  font:400 11px/1.4 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
  color:var(--silver);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.uq-fold, .uq-shut{
  width:24px; height:24px; flex:none;
  border:0; background:none; cursor:pointer;
  position:relative;
  border-radius:var(--r-sm);
}
.uq-fold:hover, .uq-shut:hover{ background:var(--glass); }
/* A chevron and a cross, drawn rather than loaded. */
.uq-fold::before{
  content:""; position:absolute; left:8px; top:9px;
  width:7px; height:7px;
  border:solid var(--silver); border-width:0 0 1.5px 1.5px;
  transform:rotate(-45deg);
  transition:transform .18s ease;
}
.uq.is-folded .uq-fold::before{ transform:rotate(135deg); top:11px; }
.uq-shut::before, .uq-shut::after{
  content:""; position:absolute; left:6px; top:11px;
  width:12px; height:1.5px; background:var(--silver);
}
.uq-shut::before{ transform:rotate(45deg); }
.uq-shut::after{ transform:rotate(-45deg); }

.uq-list{
  list-style:none; margin:0; padding:4px 0;
  max-height:min(46vh, 340px);
  overflow:auto;
  transition:max-height .22s ease, padding .22s ease;
}
.uq.is-folded .uq-list{ max-height:0; padding:0; overflow:hidden; }

.uq-row{
  display:flex; align-items:center; gap:11px;
  padding:7px 16px;
}
.uq-name{
  flex:1; min-width:0;
  font-size:13px; color:var(--bone);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.uq-row.is-waiting .uq-name{ color:var(--dim); }
.uq-pct{
  flex:none;
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.08em;
  color:var(--dimmer);
  min-width:34px; text-align:right;
}

/* Cancelling. Per row and for the whole run, because changing your mind about one
   400MB video is a different thing from changing your mind about the lot. */
.uq-all{
  flex:none;
  border:0; background:none; cursor:pointer;
  padding:4px 2px;
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--dimmer);
}
.uq-all:hover{ color:var(--bad); }

.uq-stop{
  flex:none; position:relative;
  width:20px; height:20px;
  border:0; background:none; cursor:pointer;
  border-radius:50%;
  opacity:0;
  transition:opacity .14s ease, background-color .14s ease;
}
/* Shown on hover, and always while a row is actually moving - that is the one you are
   most likely to want to stop, and hunting for a control that only appears on hover is
   no use on a phone. */
.uq-row:hover .uq-stop,
.uq-row.is-live .uq-stop{ opacity:1; }
/* On a touch screen there is no hover to reveal anything, so every row that can still
   be stopped shows its control. Without this a queued file was uncancellable on a
   phone, which is exactly where you are most likely to have picked the wrong folder. */
@media (hover: none){
  .uq-row.is-waiting .uq-stop,
  .uq-row.is-live .uq-stop{ opacity:1; }
}
.uq-stop:hover{ background:var(--glass); }
.uq-stop::before, .uq-stop::after{
  content:""; position:absolute; left:5px; top:9px;
  width:10px; height:1.5px; background:var(--dimmer);
}
.uq-stop::before{ transform:rotate(45deg); }
.uq-stop::after{ transform:rotate(-45deg); }
.uq-stop:hover::before, .uq-stop:hover::after{ background:var(--bad); }

/* Cancelled is its own state, not a failure: no red, nothing to go and look at. */
.uq-row.is-stopped .uq-arc{ opacity:0; }
.uq-row.is-stopped .uq-trk{ stroke:var(--line); }
.uq-row.is-stopped .uq-name{ color:var(--dimmer); text-decoration:line-through; }
.uq-row.is-stopped .uq-pct{ color:var(--dimmer); }

/* The ring. One SVG circle with its dash offset driven by --p, which the uploader sets
   to a fraction, so progress costs a style write rather than a redraw. */
.uq-ring{
  position:relative; flex:none;
  width:24px; height:24px;
  display:grid; place-items:center;
}
.uq-ring svg{ width:24px; height:24px; display:block; }
.uq-ring circle{ fill:none; stroke-width:2.5; }
.uq-trk{ stroke:var(--glass-3); }
.uq-arc{
  stroke:var(--bone);
  stroke-linecap:round;
  stroke-dasharray:56.549;
  stroke-dashoffset:calc(56.549 - 56.549 * var(--p, 0));
  transform:rotate(-90deg);
  transform-origin:50% 50%;
  transition:stroke-dashoffset .15s linear;
}
.uq-row.is-waiting .uq-arc{ opacity:0; }
.uq-row.is-waiting .uq-trk{ stroke:var(--line); }

/* A tick when it lands, a cross when it does not. */
.uq-mark{ position:absolute; display:none; }
.uq-row.is-done .uq-arc{ stroke:#39d98a; }
.uq-row.is-done .uq-mark{
  display:block;
  left:9px; top:6px;
  width:5px; height:10px;
  border:solid #39d98a; border-width:0 2px 2px 0;
  transform:rotate(45deg);
}
.uq-row.is-bad .uq-arc{ opacity:0; }
.uq-row.is-bad .uq-trk{ stroke:var(--bad); }
.uq-row.is-bad .uq-name{ color:var(--bad); }
.uq-row.is-bad .uq-mark{
  display:block;
  left:11px; top:7px;
  width:2px; height:10px;
  background:var(--bad);
  transform:rotate(45deg);
}
.uq-row.is-bad .uq-mark::after{
  content:""; position:absolute; inset:0;
  background:var(--bad);
  transform:rotate(90deg);
}

/* The gallery toolbar: what is shown, how big, and whether clicking picks or opens. */
.tools{
  display:flex; align-items:center; gap:18px;
  margin-left:auto;
}
.pickmode{
  display:inline-flex; align-items:center; gap:8px;
  padding:7px 13px;
  border:1px solid var(--line-2);
  border-radius:999px;
  background:none; color:var(--silver);
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
  cursor:pointer;
  transition:color .14s ease, border-color .14s ease, background-color .14s ease;
}
.pickmode:hover{ color:var(--bone); border-color:var(--bone); }
.pickmode-tick{
  width:13px; height:13px; flex:none;
  border:1px solid currentColor;
  border-radius:3px;
  position:relative;
}
.pickmode-tick::before{
  content:""; position:absolute; left:4px; top:1px;
  width:3px; height:7px;
  border:solid currentColor; border-width:0 1.5px 1.5px 0;
  transform:rotate(45deg);
}
.pickmode.on{
  background:var(--bone); border-color:var(--bone); color:var(--void);
}

.zoom{ display:flex; align-items:center; gap:9px; cursor:pointer; }
.zoom-ic{ flex:none; border:1px solid var(--dimmer); border-radius:2px; }
.zoom-sm{ width:8px; height:8px; }
.zoom-lg{ width:13px; height:13px; }
.zoom input[type="range"]{
  -webkit-appearance:none; appearance:none;
  width:104px; height:3px;
  border-radius:2px;
  background:var(--glass-3);
  cursor:pointer;
}
.zoom input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  width:13px; height:13px; border-radius:50%;
  background:var(--bone);
  border:0;
}
.zoom input[type="range"]::-moz-range-thumb{
  width:13px; height:13px; border-radius:50%;
  background:var(--bone); border:0;
}

/* Full screen. A fixed layer rather than a new tab, so the arrow keys work and closing
   puts you back exactly where you were in the grid. */
body.lb-open{ overflow:hidden; }
.lb{
  /* The bar's height, as a number the notes panel can start below. It used to open at
     top:0, and at 92vw on a phone that put it straight over the bar - including the
     close button, with no Esc key to fall back on and the backdrop behind it. There was
     no way out of the notes panel at all. */
  --lbbar:66px;
  /* The breathing room around the photograph, and the width of the notes panel. Both are
     named here because the image's own size is worked out from them, and a padding that
     drifted from the number in that sum would put the frame back off the bottom. */
  --lbpad:18px;
  --lbnotes:min(380px, 92vw);

  position:fixed; inset:0; z-index:90;
  /* dvh as well as the inset, because the on-screen keyboard shrinks the visual
     viewport when the note box takes focus and the bar would otherwise be pushed off. */
  height:100dvh;
  display:grid;
  /* minmax(0,...) rather than a bare 1fr: the automatic minimum of a 1fr track is the
     content's min-content size, so a 4000px frame grew the track instead of being told
     to fit inside it. */
  grid-template-columns:auto minmax(0, 1fr) auto;
  grid-template-rows:auto minmax(0, 1fr);
  align-items:center;
  background:rgba(4,4,6,.94);
  -webkit-backdrop-filter:blur(14px);
  backdrop-filter:blur(14px);
  opacity:0;
  transition:opacity .16s ease;
}
.lb.on{ opacity:1; }
.lb-bar{
  grid-column:1 / -1;
  /* Above the notes panel, always. Whatever else is open, the way out stays clickable. */
  position:relative; z-index:3;
  height:var(--lbbar); box-sizing:border-box;
  display:flex; align-items:center; gap:16px;
  padding:0 20px;
  background:rgba(4,4,6,.96);
  border-bottom:1px solid var(--line);
}
.lb-name{
  margin:0; flex:1; min-width:0;
  font-size:14px; color:var(--bone);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* Sits beside the filename, only for raws. Quiet rather than a warning: nothing is
   wrong, it is simply worth knowing that the frame on screen is a preview of the file
   and not the file. flex:none so a long filename shrinks before this does - the notice
   is useless truncated to "Preview qual..." */
.lb-raw{
  margin:0; flex:none;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.06em; color:var(--dim);
  padding:5px 9px; border-radius:999px;
  box-shadow:inset 0 0 0 1px var(--line);
}
@media (max-width: 620px){
  /* On a phone the header is already the filename, a count and two buttons. */
  .lb-raw{ display:none; }
}

.lb-count{
  margin:0; flex:none;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.14em; color:var(--dimmer);
}
.lb-get{
  flex:none;
  padding:8px 15px;
  border:1px solid var(--line-2); border-radius:999px;
  background:none; color:var(--silver);
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
  cursor:pointer;
}
.lb-get:hover{ color:var(--bone); border-color:var(--bone); }
.lb-x{
  flex:none; position:relative;
  width:32px; height:32px;
  border:0; background:none; cursor:pointer; border-radius:var(--r-sm);
}
.lb-x:hover{ background:var(--glass); }
.lb-x::before, .lb-x::after{
  content:""; position:absolute; left:8px; top:15px;
  width:16px; height:1.5px; background:var(--bone);
}
.lb-x::before{ transform:rotate(45deg); }
.lb-x::after{ transform:rotate(-45deg); }

.lb-stage{
  grid-row:2; grid-column:2;
  margin:0; padding:var(--lbpad);
  /* Flex, not grid. As a grid this had implicit auto tracks, which the photograph sized
     - so max-height:100% on it resolved against itself and constrained nothing, and a
     tall frame hung 677px below the fold on a short window. Nothing was visibly wrong on
     a tall one, which is why it survived. */
  display:flex; align-items:center; justify-content:center;
  width:100%; height:100%;
  min-width:0; min-height:0;
  overflow:hidden;
  /* So the frame slides rather than jumps when the notes panel opens beside it. */
  transition:padding .2s ease;
}
.lb-stage img, .lb-stage video{
  /* Height measured off the viewport rather than off an ancestor. dvh is the live
     viewport, so this holds on a phone when the browser chrome slides away and when the
     keyboard opens under the note box - and it cannot be undone by anything in between. */
  max-width:100%;
  max-height:calc(100dvh - var(--lbbar) - (var(--lbpad) * 2));
  width:auto; height:auto;
  object-fit:contain;
  display:block;
}
.lb-wait{
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.2em; text-transform:uppercase;
  color:var(--dimmer);
}
.lb-step{
  grid-row:2;
  position:relative;
  width:68px; height:100%;
  border:0; background:none; cursor:pointer;
}
.lb-prev{ grid-column:1; }
.lb-next{ grid-column:3; }
.lb-step:hover{ background:rgba(244,244,246,.04); }
.lb-step::before{
  content:""; position:absolute; top:50%; left:50%;
  width:13px; height:13px; margin:-7px 0 0 -7px;
  border:solid var(--bone); border-width:0 0 2px 2px;
}
.lb-prev::before{ transform:rotate(45deg); margin-left:-4px; }
.lb-next::before{ transform:rotate(-135deg); margin-left:-10px; }

/* A marker, not a number you read - it says somebody has talked about this frame. */
.tile-notes{
  position:absolute; left:10px; bottom:10px;
  min-width:20px; height:20px;
  padding:0 6px;
  display:grid; place-items:center;
  font:700 10px/1 "Space Mono", monospace;
  color:var(--void);
  background:var(--bone);
  border-radius:999px;
  pointer-events:none;
}

/* Which homepage gallery a frame has been chosen for. Bottom right because the other
   three corners are taken - the New badge, the note count and the selection tick - and
   all four can be on the same tile at once. */
.tile-home{
  position:absolute; right:10px; bottom:10px;
  max-width:calc(100% - 20px);
  padding:3px 8px;
  font:700 9px/1.25 "Space Mono", monospace;
  letter-spacing:.1em; text-transform:uppercase;
  -webkit-backdrop-filter:blur(3px);
  backdrop-filter:blur(3px);
  border-radius:999px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  pointer-events:none;

  /* Filled: this gallery is live on glitchedvisuals.com. */
  color:var(--void);
  background:var(--good);
  border:1px solid var(--good);
}

/* Chosen, but the site has not been rebuilt yet. The same green, because both mean the
   frame is spoken for and a badge that changed hue would read as a different kind of
   thing. Outlined rather than filled, so one is still a photograph on the website and
   the other a decision waiting on a build. */
.tile-home.is-queued{
  color:var(--good);
  background:rgba(5,5,6,.72);
  border-color:rgba(70,224,168,.45);
}

/* The thread, as a panel over the right of the full screen view rather than a column in
   it: most of the time you are looking at the picture, and the notes should not take
   width away from it until you ask for them. */
.lb-notes-go{
  flex:none;
  padding:8px 15px;
  border:1px solid var(--line-2); border-radius:999px;
  background:none; color:var(--silver);
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
  cursor:pointer;
}
.lb-notes-go:hover{ color:var(--bone); border-color:var(--bone); }
.lb-notes-go[aria-pressed="true"]{
  background:var(--bone); border-color:var(--bone); color:var(--void);
}

.lb-notes{
  position:absolute; right:0; top:var(--lbbar); bottom:0; z-index:2;
  width:var(--lbnotes);
  transform:translateX(100%);
  transition:transform .2s ease;
  display:flex; flex-direction:column;
  background:rgba(14,14,18,.96);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  border-left:1px solid var(--line);
}
.lb.has-notes .lb-notes{ transform:translateX(0); }

/* With the thread open the photograph moves out from under it instead of sitting half
   covered. Only on a wide screen: the panel is nearly the full width on a phone, and
   there is no room to move into. */
@media (min-width:621px){
  .lb.has-notes .lb-stage{ padding-right:calc(var(--lbnotes) + var(--lbpad)); }
}

.lb-thread{
  list-style:none; margin:0; padding:18px;
  flex:1; overflow:auto;
}
.lb-note + .lb-note{ margin-top:16px; padding-top:16px; border-top:1px solid var(--line); }
.lb-who{
  margin:0 0 5px;
  display:flex; align-items:baseline; gap:9px;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.1em; text-transform:uppercase;
  color:var(--silver);
}
.lb-when{ color:var(--dimmer); letter-spacing:.06em; text-transform:none; }
.lb-drop{
  margin-left:auto;
  border:0; background:none; cursor:pointer;
  font:inherit; color:var(--dimmer);
  text-transform:none; letter-spacing:.06em;
}
.lb-drop:hover{ color:var(--bad); }
.lb-body{
  margin:0;
  font-size:14px; line-height:1.65;
  color:var(--bone);
  white-space:pre-wrap; word-break:break-word;
}
.lb-none{
  font:400 11px/1.6 "Space Mono", monospace;
  letter-spacing:.1em; text-transform:uppercase;
  color:var(--dimmer);
}

.lb-say{
  padding:14px 18px 18px;
  border-top:1px solid var(--line);
  display:flex; flex-direction:column; gap:10px;
}
.lb-say textarea{
  width:100%;
  padding:10px 12px;
  font:inherit; font-size:14px; line-height:1.55;
  color:var(--bone);
  background:var(--ink-3);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  resize:vertical;
}
.lb-say textarea:focus{ outline:none; border-color:var(--line-2); }
.lb-say .act{ align-self:flex-end; }

@media (max-width:620px){
  .lb{ --lbbar:58px; --lbpad:12px; }

  /* The arrows go over the frame instead of taking a column out of it. Beside it they
     cost 92px of a 320px screen, which left a tall photograph 192px wide and using a
     third of the height it had - the frame fitted, but barely any of the page was the
     photograph. Over it they cost nothing: the stage has no tap target of its own, so
     there is nothing underneath for them to steal. */
  .lb{ grid-template-columns:minmax(0, 1fr); }
  .lb-stage{ grid-column:1; }
  .lb-step{
    position:absolute; top:var(--lbbar); bottom:0; z-index:2;
    width:56px; height:auto;
  }
  .lb-prev{ left:0; }
  .lb-next{ right:0; }
  .lb-bar{ padding:0 14px; gap:10px; }
  .lb-get{ display:none; }
  /* The panel is nearly the full width here, so the arrows beneath it are unreachable
     anyway and only confuse what is tappable. */
  .lb.has-notes .lb-step{ display:none; }
  .tools{ width:100%; margin-left:0; justify-content:space-between; }
}

/* A shared login can be either of them, so the name in the bar is a control rather than
   a label. Underlined on hover only - it should not shout, it is read far more often
   than it is clicked. */
.who-switch{ cursor:pointer; }
.who-switch:hover{ color:var(--bone); text-decoration:underline; text-underline-offset:3px; }

/* The invitation line. A small filled dot rather than the word "yes", so the two states
   are told apart at a glance without reading either of them. */
.sub-invite .sent{ display:inline-flex; align-items:center; gap:7px; }
.sub-invite .sent::before{
  content:""; flex:0 0 auto;
  width:6px; height:6px; border-radius:50%;
  background:var(--good);
}

/* ------------------------------------------------------------------ layout switch */

/* align-items:center is doing real work here. On a phone the Select pill beside these
   is min-height:42px for a thumb, which stretches the row to 42px - and .lay has a
   fixed 30px height, so without this the two buttons sat at the TOP of that row while
   Select filled it. They looked misaligned because they were. */
.layouts{
  display:flex; align-items:center; gap:6px;
  flex:0 0 auto; margin-left:auto;
}

/* Two drawn glyphs, no icon font: three bars for rows, four squares for cards. */
.lay{
  position:relative;
  width:30px; height:30px;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:none;
  cursor:pointer;
  transition:border-color .14s ease, background-color .14s ease;
}
.lay:hover{ border-color:var(--line-2); }
.lay.on{ background:var(--bone); border-color:var(--bone); }

.lay::before,
.lay::after{ content:""; position:absolute; background:var(--dim); transition:background-color .14s ease; }
.lay.on::before, .lay.on::after{ background:var(--void); }

/* rows: three stacked bars, 16 x 12 all told (2 + 3 + 2 + 3 + 2 tall).

   CENTRED FROM THE MIDDLE, NOT INSET FROM THE EDGES. These were written as left:8px
   right:8px top:9px, which centres a glyph in a 30px button and in nothing else - and
   .lay is 42px under (hover:none) so a thumb can hit it. On a phone the bars sat 6px
   high and stretched to 24px wide, while the squares beside them sat low and right.
   Hung off the centre they are correct at any button size, including the next one. */
.lay-rows::before{
  left:50%; top:50%;
  margin-left:-8px; margin-top:-6px;
  width:16px; height:2px;
  box-shadow:0 5px 0 var(--dim), 0 10px 0 var(--dim);
}
.lay-rows.on::before{ box-shadow:0 5px 0 var(--void), 0 10px 0 var(--void); }
.lay-rows::after{ display:none; }

/* cards: a two by two grid, 14 x 14 all told (5 + 4 + 5). Centred the same way. */
.lay-cards::before{
  left:50%; top:50%;
  margin-left:-7px; margin-top:-7px;
  width:5px; height:5px;
  box-shadow:9px 0 0 var(--dim), 0 9px 0 var(--dim), 9px 9px 0 var(--dim);
}
.lay-cards.on::before{ box-shadow:9px 0 0 var(--void), 0 9px 0 var(--void), 9px 9px 0 var(--void); }
.lay-cards::after{ display:none; }

/* Somewhere to hang the switch when the heading above it belongs to something else. */
.shelf-bar{ display:flex; justify-content:flex-end; margin:22px 0 -8px; }
/* A bar carrying an action as well as the layout toggles stacks them, toggles first.
   Side by side the button and the toggles read as one group of three controls; stacked,
   the toggles stay a setting and the button stays a thing you do. */
.shelf-bar-stack{ flex-direction:column; align-items:flex-end; gap:12px; }

/* ------------------------------------------------------------------ cards */

/* The picture does the work. Names underneath, detail smaller still - a wall you scan
   with your eyes rather than read. */
.cards{
  margin:24px 0 0; padding:0;
  list-style:none;
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, 230px), 1fr));
  gap:clamp(14px, 1.8vw, 24px);
}

.card{ min-width:0; }
.card a{ display:block; text-decoration:none; color:inherit; }

.card-shot{
  position:relative;
  display:grid; place-items:center;
  aspect-ratio:4 / 5;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--ink-3);
  overflow:hidden;
  transition:border-color .16s ease, transform .18s ease;
}
.card a:hover .card-shot{ border-color:var(--line-2); transform:translateY(-2px); }
.card-shot img{ width:100%; height:100%; object-fit:cover; display:block; }

.card-ph{
  width:100%; height:100%;
  display:grid; place-items:center;
  background:var(--holo);
  color:var(--bone);
  font:700 clamp(26px, 4vw, 40px)/1 "Space Mono", monospace;
  letter-spacing:.06em;
}

.card-dot{ position:absolute; right:10px; top:10px; margin:0; }

.card-name{
  display:flex; align-items:center; min-width:0;
  margin:11px 0 0;
  font-size:13.5px; color:var(--bone);
}
.card-sub{
  display:block;
  margin:3px 0 0;
  font-size:11.5px; color:var(--dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.card-meta{
  display:block;
  margin:7px 0 0;
  font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--dimmer);
}
.card-foot{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  margin:9px 0 0;
}
/* In a row there is width to spare and the side sits on one line. In a card there is
   not: the account tag plus the project summary is wider than the card, and without
   this they run straight across the next one. */
.card-foot .row-side{
  padding:0;
  flex-wrap:wrap;
  min-width:0; max-width:100%;
  gap:8px 12px;
}
.card-foot .row-meta{ white-space:normal; text-align:left; }

.card.is-off .card-shot{ opacity:.45; }
.card.is-off .card-name{ color:var(--dim); }

/* ------------------------------------------------------------------ activity */

.day{ margin:26px 0 0; }
.day-head{
  margin:0 0 2px;
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.22em; text-transform:uppercase;
  color:var(--dimmer);
  padding:0 0 10px;
  border-bottom:1px solid var(--line);
}

.log{ margin:0; padding:0; list-style:none; }

.log-row{
  display:grid;
  grid-template-columns:56px minmax(0,1fr);
  gap:14px;
  align-items:baseline;
  border-bottom:1px solid var(--line);
  position:relative;
}

/* A hairline in the client colour marks the rows that were not us. At a glance you can
   see the shape of a day: your own work in one column, theirs standing out of it. */
.log-row.is-client::before{
  content:"";
  position:absolute; left:-10px; top:12px; bottom:12px; width:2px;
  background:var(--violet);
}

.log-time{
  font-size:10.5px; letter-spacing:.1em;
  color:var(--dimmer);
  padding:13px 0;
  white-space:nowrap;
}

.log-body{
  display:block;
  padding:13px 2px;
  text-decoration:none; color:inherit;
  min-width:0;
}
a.log-body:hover{ background:rgba(244,244,246,.03); }

.log-line{ display:block; font-size:13.5px; color:var(--dim); line-height:1.5; }
.log-line strong{ color:var(--bone); font-weight:400; }
.log-line em{ font-style:normal; color:var(--bone); }
.log-row.is-client .log-line strong{ color:var(--violet); }

.log-detail{
  display:block;
  margin:3px 0 0;
  font-size:11.5px; color:var(--dimmer); line-height:1.5;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

@media (max-width:560px){
  .log-row{ grid-template-columns:46px minmax(0,1fr); gap:10px; }
}

/* ------------------------------------------------------------------ the bell */

.bell-wrap{ position:relative; flex:0 0 auto; }

.bell{
  --bell:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22a2.5 2.5 0 0 0 2.45-2h-4.9A2.5 2.5 0 0 0 12 22zm7-5.5-1.5-1.6V10a5.5 5.5 0 0 0-4.25-5.36V4a1.25 1.25 0 0 0-2.5 0v.64A5.5 5.5 0 0 0 6.5 10v4.9L5 16.5V18h14z'/%3E%3C/svg%3E");
  position:relative;
  width:34px; height:34px;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:none;
  cursor:pointer;
  transition:border-color .14s ease;
}
.bell::after{
  content:"";
  position:absolute; inset:8px;
  background:var(--dim);
  -webkit-mask:var(--bell) center/contain no-repeat;
  mask:var(--bell) center/contain no-repeat;
  transition:background-color .14s ease;
}
.bell:hover{ border-color:var(--line-2); }
.bell:hover::after{ background:var(--bone); }
.bell.has-new::after{ background:var(--bone); }

/* The count. Holo rather than the usual red: this is work arriving, not an error. */
.bell-n{
  position:absolute; top:-6px; right:-6px;
  min-width:17px; height:17px;
  padding:0 4px;
  border-radius:9px;
  background:var(--holo);
  color:var(--bone);
  font:700 9px/17px "Space Mono", monospace;
  text-align:center;
  box-shadow:0 0 0 2px var(--void);
}

.bell-panel{
  /* Fixed rather than hung off the bell, because it no longer shares a parent with it.
     The offsets match the bar's own height and side padding, so it still lines up under
     the bell exactly as before. */
  position:fixed; right:clamp(16px,3.5vw,32px); top:72px;
  width:min(420px, calc(100vw - 32px));
  max-height:min(70vh, 540px);
  overflow:auto;
  /* Real glass: you can see there is something behind it, but not what. The fill stays
     light and the blur does the work - at this radius the text underneath dissolves into
     a wash of its own colour instead of staying readable. Raising the fill instead would
     have given a solid panel, which is a different thing entirely. */
  background:rgba(16,16,20,.55);
  -webkit-backdrop-filter:blur(40px) saturate(170%);
  backdrop-filter:blur(40px) saturate(170%);
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  box-shadow:0 24px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(244,244,246,.06);
  z-index:40;
}
.bell-head{
  display:flex; justify-content:space-between; align-items:center;
  padding:13px 15px;
  border-bottom:1px solid var(--line);
  font-size:10px; letter-spacing:.2em; text-transform:uppercase; color:var(--dim);
  position:sticky; top:0;
  /* A touch denser than the panel, so the heading stays legible as rows scroll under it,
     but still glass rather than a lid. */
  background:rgba(16,16,20,.72);
  -webkit-backdrop-filter:blur(40px) saturate(170%);
  backdrop-filter:blur(40px) saturate(170%);
}
.bell-list{ margin:0; padding:0; list-style:none; }
.bell-list li{ border-bottom:1px solid var(--line); position:relative; }

.bell-head-acts{ display:flex; align-items:center; gap:14px; }

/* The preview pass, while it runs. A bar at the top of the page rather than a dialog:
   it takes a while, and nothing else on the page needs blocking while it does. */
.backfill{
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  margin:0 0 18px;
  padding:11px 15px;
  font-size:12.5px;
  color:var(--silver);
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:rgba(244,244,246,.04);
}
.backfill-say{
  min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* The two date boxes, inline with the chips they belong to rather than on a row of
   their own, so the filter bar stays one line until it has to wrap. */
.act-range{ display:flex; align-items:center; gap:7px; }
.act-range input[type="date"]{
  padding:5px 8px;
  font:400 11px/1.3 "Space Mono", monospace;
  color:var(--silver);
  background:rgba(244,244,246,.043);
  border:1px solid var(--line-2);
  border-radius:8px;
  color-scheme:dark;
}
.act-range input[type="date"]:focus-visible{
  outline:1px solid var(--bone); outline-offset:1px;
}
.act-range-to{
  font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--dimmer);
}

/* One notice at a time, over the right of its own row. Hidden until the row is under
   the pointer: this list is read far more often than it is cleared, and a column of
   crosses down the side reads as clutter the rest of the time. */
.bell-x{
  position:absolute; right:9px; top:9px;
  width:22px; height:22px;
  display:grid; place-items:center;
  padding:0; margin:0;
  font:400 15px/1 system-ui, sans-serif;
  cursor:pointer;
  color:var(--dim);
  background:rgba(16,16,20,.9);
  border:1px solid var(--line-2);
  border-radius:999px;
  opacity:0;
  transition:opacity .12s ease, color .12s ease, border-color .12s ease;
}
.bell-list li:hover .bell-x,
.bell-list li:focus-within .bell-x{ opacity:1; }
.bell-x:hover{ color:var(--bad, #ff6b6b); border-color:var(--bad, #ff6b6b); }
.bell-x:focus-visible{ opacity:1; outline:1px solid var(--bone); outline-offset:2px; }
@media (hover:none){ .bell-x{ opacity:1; } }

/* Room for the cross, so a long "when" does not slide under it. Qualified by the li so
   it outranks the shorthand padding set on .bell-list a further down, rather than
   depending on which rule happens to come last. */
.bell-list li a{ padding-right:38px; }
.bell-list a{
  display:grid;
  grid-template-columns:auto 1fr auto;
  grid-template-areas:"kind who when" "body body body";
  gap:4px 10px;
  padding:13px 15px;
  text-decoration:none; color:inherit;
}
.bell-list a:hover{ background:rgba(244,244,246,.04); }

/* Unread carries the accent bar. One glance tells you how much is new without counting. */
.bell-list li.is-new{ position:relative; }
.bell-list li.is-new::before{
  content:"";
  position:absolute; left:0; top:10px; bottom:10px; width:2px;
  background:var(--line-accent);
}
.bell-list li:not(.is-new) a{ opacity:.6; }

.nk{
  grid-area:kind;
  font-size:8.5px; letter-spacing:.14em; text-transform:uppercase;
  padding:3px 6px; border-radius:var(--r-sm);
  border:1px solid var(--line);
  color:var(--dim);
  white-space:nowrap;
}
/* Violet asks something of us; blue is something that simply happened. Two colours
   carry the only distinction that changes what you do next. */
.nk-print{ color:var(--violet); border-color:rgba(123,63,224,.5); }
.nk-note{  color:var(--violet); border-color:rgba(123,63,224,.5); }
.nk-brief{ color:var(--violet); border-color:rgba(123,63,224,.5); }
.nk-open{  color:var(--blue);   border-color:rgba(47,139,240,.4); }

.bell-who{ grid-area:who; font-size:12.5px; color:var(--bone); align-self:center;
           overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bell-when{ grid-area:when; font-size:9.5px; letter-spacing:.1em; text-transform:uppercase;
            color:var(--dimmer); align-self:center; white-space:nowrap; }
.bell-body{ grid-area:body; font-size:11.5px; color:var(--dim); line-height:1.5; }
.bell-empty{ margin:0; padding:22px 15px; font-size:12px; line-height:1.6; color:var(--dim); }

/* ------------------------------------------------------------------ documents */

/* A sheet of paper with the corner turned, drawn rather than fetched. */
.av-doc{
  background:var(--ink-3);
  border:1px solid var(--line);
  position:relative;
}
.av-doc::before{
  content:"";
  position:absolute; left:11px; right:11px; top:8px; bottom:8px;
  border:1px solid var(--dim);
  border-top-right-radius:0;
  clip-path:polygon(0 0, 62% 0, 100% 26%, 100% 100%, 0 100%);
}
.av-doc::after{
  content:"";
  position:absolute; right:11px; top:8px;
  width:7px; height:7px;
  border-left:1px solid var(--dim);
  border-bottom:1px solid var(--dim);
}

/* The format, printed on the page. Three or four letters is the whole label: anybody
   who has used a computer reads PDF faster than they read any drawing of one. */
.av-doc em{
  position:relative;              /* above the drawn sheet, not behind it */
  font:700 9px/1 "Space Mono", monospace;
  letter-spacing:.1em;
  font-style:normal;
  color:var(--dim);
  padding-top:6px;                /* clear of the folded corner */
}

/* One colour per format. The sheet keeps its drawn outline and takes the colour with
   it, so the tile reads as a tinted page rather than a coloured box. */
.av-doc.dk-pdf   { --dk:#e5484d; }
.av-doc.dk-word  { --dk:#3b82f6; }
.av-doc.dk-sheet { --dk:#22c55e; }
.av-doc.dk-deck  { --dk:#f97316; }
.av-doc.dk-img   { --dk:#a855f7; }
.av-doc.dk-text  { --dk:#94a3b8; }
.av-doc.dk-zip   { --dk:#e8b23a; }
.av-doc.dk-any   { --dk:#8e9199; }

.av-doc[class*="dk-"]{
  border-color:color-mix(in srgb, var(--dk) 38%, transparent);
  background:color-mix(in srgb, var(--dk) 9%, var(--ink-3));
}
.av-doc[class*="dk-"]::before,
.av-doc[class*="dk-"]::after{ border-color:color-mix(in srgb, var(--dk) 70%, transparent); }
.av-doc[class*="dk-"] em{ color:var(--dk); }
.row-meta .linky + .linky{ margin-left:12px; }

/* A document row in select mode. The tick box sits where it does in every other list;
   the row itself is the hit area, because aiming at a 20px circle on a phone is a
   worse version of the same gesture. */
#doc-list li[data-row]{ cursor:default; }
#doc-list li.is-picked{ background:var(--glass); }

/* Being dragged. Faded rather than hidden, so you can still see where it came from. */
#doc-list li.is-lifted{ opacity:.45; }

/* A folder, or the way back up, while a document is over it. */
.rows.folders .row-link.drop-on,
a.back.drop-on{
  background:var(--glass-2);
  border-radius:var(--r-sm);
  outline:1px dashed var(--line-2);
  outline-offset:2px;
}

/* ------------------------------------------------------------------ folders */

/* A folder reads as a folder before it reads as a word, so it gets the same slot an
   avatar would and a drawn glyph rather than initials. */
/* The drawn glyphs stay square. A folder is not a photograph and stretching it to 4:5
   just makes a tall thin folder. */
.av-folder,
.av-doc{ height:64px; align-self:center; }

/* Drawn as one path rather than built out of three boxes. The box version had to fake
   the tab with a second rectangle sitting on the first, which never quite lined up and
   read as a sheet of paper with a corner missing. */
.av-folder{
  background:var(--ink-3);
  border:1px solid var(--line);
  display:grid; place-items:center;
  color:var(--dim);
}
.av-folder svg{ width:30px; height:30px; display:block; }
.rows li:hover .av-folder{ color:var(--silver); }
/* Three questions stacked instead of one long line of chips. At a single row the
   status, the deadline and the type ran together and nothing said which chip belonged
   to which question. */
.filters-stack{
  display:flex; flex-direction:column; align-items:flex-start; gap:10px;
}
.filters-stack .chips{ flex-wrap:wrap; }

.rows.folders{ margin-top:22px; }

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

/* Three dots at the end of a row. A <details>, so Escape and the tab order come free.
   The marker is removed in two ways because Safari still wants the WebKit one. */
.rowmenu{ position:relative; }
.rowmenu > summary{ list-style:none; }
.rowmenu > summary::-webkit-details-marker{ display:none; }

.rowmenu-go{
  width:30px; height:30px;
  display:grid; place-items:center;
  cursor:pointer;
  font-size:17px; line-height:1;
  color:var(--dim);
  border:1px solid transparent;
  border-radius:var(--r-sm);
  transition:color .12s ease, border-color .12s ease, background-color .12s ease;
}
.rowmenu-go:hover,
.rowmenu[open] .rowmenu-go{
  color:var(--bone);
  border-color:var(--line-2);
  background:rgba(244,244,246,.05);
}
.rowmenu-go:focus-visible{ outline:1px solid var(--bone); outline-offset:2px; }

.rowmenu-pop{
  position:absolute; right:0; top:calc(100% + 5px);
  z-index:40;
  min-width:148px;
  display:flex; flex-direction:column;
  padding:5px;
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  background:rgba(16,16,20,.96);
  -webkit-backdrop-filter:blur(30px) saturate(170%);
  backdrop-filter:blur(30px) saturate(170%);
  box-shadow:0 14px 34px rgba(0,0,0,.55);
}
.rowmenu-pop button{
  padding:8px 10px;
  text-align:left;
  cursor:pointer;
  /* A button does not inherit the page's font unless told to, which left this menu in
     the browser's default sans while every row around it was mono. */
  font:inherit;
  font-size:12.5px;
  color:var(--silver);
  background:none; border:0;
  border-radius:6px;
}
.rowmenu-pop button:hover{ color:var(--void); background:var(--bone); }
.rowmenu-pop button.is-bad:hover{ color:var(--void); background:var(--bad, #ff6b6b); }
.rowmenu-pop button:focus-visible{ outline:1px solid var(--bone); outline-offset:-1px; }

/* The last row's menu would otherwise open past the bottom of a short list and be cut
   off by nothing at all - but a row near the foot of the page is common enough that it
   is worth flipping upward there. */
.rows li:last-child .rowmenu-pop{ top:auto; bottom:calc(100% + 5px); }

.crumb-sep{ color:var(--dimmer); margin:0 2px; }
.sub a{ color:var(--silver); text-decoration:none; border-bottom:1px solid var(--line); }
.sub a:hover{ color:var(--bone); border-bottom-color:var(--line-2); }

/* ------------------------------------------------------------------ pickers */

/* The browser's own date picker, made to match everything around it. A date input sizes
   itself from its own internal fields rather than from padding, so without a height it
   sits visibly shorter than the text inputs stacked above it. */
.f-date{ color-scheme:dark; min-height:44px; }
.f-date::-webkit-calendar-picker-indicator{ filter:invert(.7); cursor:pointer; }

.ask select{
  width:100%;
  background:var(--void);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  color:var(--bone);
  font:inherit; font-size:14px;
  padding:12px 13px;
}
.ask select:focus{
  outline:none; border-color:var(--line-2);
  box-shadow:0 0 0 1px rgba(47,139,240,.35);
}

/* Firefox draws the popup list itself and does not always take its colour from
   color-scheme, so each option is painted outright as well. */
select option{
  background:var(--ink-3);
  color:var(--bone);
}

/* A group heading in a dropdown. Left alone it inherits the browser's own idea of a
   disabled label, which on a dark popup is grey on near-black and effectively invisible
   - the headings were there and could not be read. Stated outright, and lighter than
   the names under it rather than dimmer, because it is the thing you are scanning for. */
select optgroup{
  background:var(--ink-3);
  color:var(--bone);
  font-weight:700;
  font-style:normal;
}
/* Restated: some browsers hand the optgroup's colour down to the options inside it. */
select optgroup option{
  background:var(--ink-3);
  color:var(--silver);
  font-weight:400;
}

/* Preview beside the controls, so you can see what you picked before it uploads - and
   before anything leaves the machine. */
.pickr{
  display:flex; align-items:center; gap:14px;
  margin:0 0 10px;
}
.pickr-shot{
  width:64px; height:64px; flex:0 0 auto;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--void);
  display:grid; place-items:center;
  overflow:hidden;
}
.pickr-shot img{ width:100%; height:100%; object-fit:cover; display:block; }
.pickr-shot em{
  font-style:normal; font-size:9px; letter-spacing:.16em;
  text-transform:uppercase; color:var(--dimmer);
}
.pickr-side{ display:flex; flex-direction:column; align-items:flex-start; gap:8px; }
.pickr-go{
  background:none;
  border:1px solid var(--line-2);
  border-radius:var(--r-sm);
  color:var(--bone);
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase;
  padding:9px 12px; cursor:pointer;
}
.pickr-go:hover{ background:rgba(244,244,246,.05); }
.pickr-side .linky{ font-size:10px; letter-spacing:.14em; text-transform:uppercase; }

.pickr-hint{
  font-size:10px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--dimmer);
  margin-left:auto;
}

/* Dropping. The whole row is the target, not the 64px preview, because aiming at a
   thumbnail with a file in hand is a worse experience than it sounds. */
.pickr{
  border:1px dashed transparent;
  border-radius:var(--r-sm);
  padding:6px;
  margin-left:-6px; margin-right:-6px;
  transition:border-color .14s ease, background-color .14s ease;
}
.pickr.over{
  border-color:var(--holo-b);
  background:rgba(47,139,240,.07);
}
.pickr.over .pickr-hint{ color:var(--holo-b); }

/* Dropping a shoot onto the gallery. An outline on the page rather than a modal
   overlay: you should still be able to see where the files are going to land. */
.page.dropping{
  outline:2px dashed var(--holo-b);
  outline-offset:-10px;
  background:rgba(47,139,240,.04);
}

/* ------------------------------------------------------------------ phone */

@media (max-width:560px){
  /* Two cards across. One 430px-tall tile per screen is a slideshow, not a list you
     can scan - and at this width two still show the photograph properly. */
  .cards{ grid-template-columns:repeat(2, minmax(0,1fr)); gap:12px; }
  .card-name{ font-size:12.5px; }
  .card-sub{ font-size:11px; }
  .card-meta{ font-size:9.5px; }
  .card-foot{ margin-top:7px; }

  /* The gallery too: two up rather than one enormous frame. */
  .grid{ grid-template-columns:repeat(2, minmax(0,1fr)); gap:12px; }
  .tile-name{ font-size:11.5px; }
  .tile-meta{ font-size:9px; }

  /* Rows: the picture stays, the furniture shrinks. */
  .av{ width:52px; height:65px; }
  .rows a, .rows li > .row-main, .rows li:not(:has(a)){ padding:14px 2px; }
  .row-main{ font-size:14px; }
  .row-sub{ font-size:11.5px; }

  /* The status dropdown and the date stop competing for one line. */
  .rows li:has(.row-link){ gap:10px; }
  .row-side{ flex-direction:column; align-items:flex-end; gap:6px; }

  /* The side column is `auto` in the row grid and its text is nowrap, so it took every
     pixel it wanted and the name took what was left - which for "1 ready, 1 in progress"
     on a 400px screen was two pixels, and the client's name simply was not on screen.
     Capped and allowed to wrap: it costs the summary a second line and gives the name
     back more than half the row. */
  .rows li:has(.row-link),
  .rows li:has(.row-side){ grid-template-columns:minmax(0,1fr) minmax(0,36%); }
  .row-side .row-meta{ white-space:normal; }

  /* 18px between the thumbnail, the name and the meta is a desktop measure. On a phone
     those two gaps were costing the name a third of what it had left. */
  .rows a, .rows li:not(:has(a)){ column-gap:12px; }

  .head{ gap:12px; }
  .layouts{ margin-left:0; }

  /* ---- filters, on a phone ----

     Flex-wrapped, the groups ran into each other: five chips landed three-and-two at
     their own natural widths, each group started at a different x depending on how long
     its label was, and the whole block read as spare parts rather than a set of
     controls. Each group now gets a band of its own - the label on its own line, the
     chips on an even three-column grid - so every group starts at the same left edge
     and every chip is the same width as the one above it. */
  .filters{ gap:16px 0; }
  .filters .chips{
    flex:1 1 100%;
    display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
    gap:7px;
  }
  .filters .chips-label{ grid-column:1 / -1; margin:0; }
  .filters .chip{ padding:13px 8px; text-align:center; }
  /* A select or a date range is one control, not one of three. */
  .filters .act-thing,
  .filters .act-range{ grid-column:1 / -1; width:100%; }
  .filters > .linky{ flex:1 1 100%; text-align:left; }

  /* The page's own action - Add project, Add client - sits full width under the title.
     Wrapped, it was a short button stranded on the left with the filters hard against
     it, which is the thing that read as untidy. */
  .head > .act{ flex:1 1 100%; }
  /* Full width on a phone, with the gap either side. top is set when it opens, from the
     bar's measured height - the bar is three rows here and one on a desktop. */
  .bell-panel{ position:fixed; left:12px; right:12px; width:auto; }
}

/* Touch: nothing you have to aim at should be smaller than a fingertip. These only
   apply where there is no mouse, so the desktop keeps its tighter spacing. */
@media (hover:none){
  .bar-nav a{ padding:13px 0; }
  .who-out{ padding:13px 15px; }
  .bell{ width:42px; height:42px; }
  .lay{ width:42px; height:42px; }
  .chip{ padding:13px 15px; }
  .back{ padding:12px 0; }
  .st-pick{ padding:12px 26px 12px 11px; }
  .pickr-go{ padding:13px 15px; }

  /* A text button still needs a thumb's worth of room around it. */
  .linky{ display:inline-block; padding:11px 0; }
  .row-meta .linky, .bulk .linky{ padding:11px 0; }

  /* The pen keeps its drawn size; only the button it sits in grows. */
  .pen{
    width:44px; height:44px;
    margin:0 0 .08em .15em;
    -webkit-mask:var(--pen) center / 21px 21px no-repeat;
    mask:var(--pen) center / 21px 21px no-repeat;
  }

  /* Selecting a frame: the whole corner, not a 20px box. */
  .tile-pick{ width:58px; height:58px; }

  /* The one create button on each screen, and the controls beside a row. */
  .act{ padding:15px 18px; }
  .st-pick{ padding:13px 26px 13px 12px; }
  .row-meta .linky{ padding:11px 7px; }
  .pen-row{ width:44px; height:44px; margin-left:10px;
            -webkit-mask:var(--pen) center / 18px 18px no-repeat;
            mask:var(--pen) center / 18px 18px no-repeat; }
  .pickr-side .linky{ padding:11px 4px; }

  /* Form controls in the dialog. A date field in particular has a picker to tap, and
     Safari renders it shorter than a text input unless told otherwise. */
  .ask input, .ask select{ padding:14px 13px; min-height:48px; }
  .f-date{ min-height:48px; }
  .pickr-go{ min-height:46px; }
  .ask-cancel, .ask-ok{ min-height:48px; }
  .ask-x{ width:46px; height:46px; right:8px; top:8px; }
}

@media (prefers-reduced-motion:reduce){
  *{ animation:none !important; transition:none !important; }
}

/* ------------------------------------------------------------------ glass */

/* Surfaces that hold something - a photograph, a form field, a chip - get a fill you
   can see through rather than a flat panel colour. The page's own background wash
   shows through, which is what stops a dark interface reading as a black rectangle. */
.tile-shot,
.card-shot,
.pickr-shot,
.av-folder,
.av-doc{
  background:var(--glass);
  border-color:var(--glass-line);
}

input[type=email],
input[type=password],
input[type=text],
.ask select{
  background:var(--glass);
  border-color:var(--glass-line);
}
input:focus, .ask select:focus{ background:var(--glass-2); }

/* Quiet controls: visible as surfaces, not as outlines. */
.chip,
.lay,
.bell,
.who-out,
.pickr-go,
.ask-cancel,
.st-pick{
  background-color:var(--glass);
  border-color:var(--glass-line);
}
.chip:hover, .lay:hover, .bell:hover, .who-out:hover,
.pickr-go:hover, .ask-cancel:hover{ background-color:var(--glass-2); }

/* The select keeps its drawn arrow, so only the colour underneath it changes. */
.st-pick{
  background:
    linear-gradient(45deg, transparent 50%, currentColor 50%) calc(100% - 12px) 50% / 4px 4px no-repeat,
    linear-gradient(135deg, currentColor 50%, transparent 50%) calc(100% - 8px) 50% / 4px 4px no-repeat,
    var(--glass);
}

/* A selected chip stays solid - the one thing on screen that is definitely on. */
.chip.on, .lay.on{ background-color:var(--bone); border-color:var(--bone); }

/* Rows lift rather than light up. */
.rows a:hover{ background:var(--glass); border-radius:var(--r-sm); }
a.log-body:hover{ background:var(--glass); border-radius:var(--r-sm); }
.bell-list a:hover{ background:var(--glass); }

/* The one place the gradient still earns its keep: an avatar with no photograph. */
.av, .card-shot, .tile-shot, .pickr-shot{ border-radius:var(--r-sm); }
.av-big{ border-radius:var(--r); }

/* ------------------------------------------------------------------ cropping

   The picture moves under a fixed frame, not the other way round. It is the same
   arithmetic either way, but it is what a hand expects: you slide the photograph to
   choose what shows in the hole, as you would a print under a mount.

   The frame is drawn as a hole rather than as a box, by a shadow the size of the stage
   spilling inwards from the frame's own edges. One element, no overlay panes to keep in
   step with it, and the dimmed part is genuinely outside the crop rather than
   approximately so. */

/* A <dialog> opened with showModal(), so it lives in the top layer alongside the ask
   dialog that opened it rather than underneath. Everything here is undoing the
   browser's own dialog styling - a centred box with a border and a white background -
   and turning it back into a full-screen overlay. */
.crop{
  position:fixed; inset:0; z-index:200;
  width:100%; height:100%; max-width:none; max-height:100%;
  margin:0; padding:16px; border:0;
  background:transparent;        /* the dimming is ::backdrop's job now */
  color:var(--bone);
  overflow:visible;
}
/* display is set here rather than on .crop, because the browser's own rule for a
   closed dialog is display:none and it must keep winning while the dialog is shut. */
.crop[open]{ display:flex; align-items:center; justify-content:center; }
.crop::backdrop{
  background:rgba(5,5,6,.82);
  -webkit-backdrop-filter:blur(6px);
  backdrop-filter:blur(6px);
}

.crop-panel{
  width:min(560px, 100%);
  max-height:calc(100dvh - 32px);
  display:flex; flex-direction:column; gap:14px;
  padding:20px;
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  background:rgba(18,18,22,.86);
  box-shadow:0 30px 70px rgba(0,0,0,.6);
}

.crop-title{
  margin:0;
  font:700 12px/1 "Space Mono", monospace;
  letter-spacing:.18em; text-transform:uppercase; color:var(--dim);
}

.crop-stage{
  position:relative;
  flex:1; min-height:240px;
  height:min(46dvh, 360px);
  overflow:hidden;
  border-radius:var(--r-sm);
  background:
    repeating-conic-gradient(#121214 0 25%, #0b0b0d 0 50%) 0 0 / 18px 18px;
  cursor:grab;
  touch-action:none;          /* the drag is ours, not the browser's scroll */
  user-select:none;
}
.crop-stage.dragging{ cursor:grabbing; }

.crop-stage img{
  position:absolute; top:50%; left:50%;
  max-width:none;             /* the size is set in script, in pixels */
  pointer-events:none;
}

.crop-frame{
  position:absolute; top:50%; left:50%;
  transform:translate(-50%, -50%);
  pointer-events:none;
  border:1px solid rgba(244,244,246,.9);
  box-shadow:0 0 0 9999px rgba(5,5,6,.62);
}
/* Thirds, faint. Enough to put a horizon on, not enough to look like a tool. */
.crop-frame span{
  position:absolute; inset:0;
  background:
    linear-gradient(to right, transparent 33.33%, rgba(244,244,246,.18) 33.33% calc(33.33% + 1px),
      transparent calc(33.33% + 1px) 66.66%, rgba(244,244,246,.18) 66.66% calc(66.66% + 1px),
      transparent calc(66.66% + 1px)),
    linear-gradient(to bottom, transparent 33.33%, rgba(244,244,246,.18) 33.33% calc(33.33% + 1px),
      transparent calc(33.33% + 1px) 66.66%, rgba(244,244,246,.18) 66.66% calc(66.66% + 1px),
      transparent calc(66.66% + 1px));
}

.crop-shapes{ display:flex; flex-wrap:wrap; gap:6px; }
.crop-shapes .chip.on{
  color:var(--void); background:var(--bone); border-color:var(--bone);
}

.crop-zoom{ display:flex; align-items:center; gap:12px; }
.crop-zoom span{
  flex:none;
  font:400 10px/1 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase; color:var(--dim);
}
.crop-zoom input{ flex:1; accent-color:var(--blue); }

.crop-hint{
  margin:0;
  font-size:12px; line-height:1.5; color:var(--dimmer);
}

.crop-acts{
  display:flex; align-items:center; justify-content:flex-end; gap:14px;
}

@media (max-width: 520px){
  .crop-panel{ padding:14px; gap:11px; }
  .crop-stage{ height:min(40dvh, 300px); min-height:200px; }
  /* The hint is the one line a thumb has already worked out for itself. */
  .crop-hint{ display:none; }
}

/* ------------------------------------------------------------------ studio chat

   Bottom right, above everything except the lightbox and the crop dialog, which are
   both full-screen and have the floor while they are open. Managers only - it is never
   built on a client's page, so there is nothing here to hide from one. */

.chat{
  position:fixed; right:18px; bottom:18px; z-index:90;
  display:flex; flex-direction:column; align-items:flex-end; gap:10px;
  /* The container itself catches nothing. On a phone it stretches to left:12px so the
     presence pill can wrap, which made it an invisible full-width strip sitting on top
     of the warnings bubble in the opposite corner - same z-index, later in the DOM, so
     it won. The bubble was drawn, lit, and completely untappable. Only the pills inside
     take taps now, which is the whole fix and also stops it happening again to whatever
     lands in that corner next. */
  pointer-events:none;
}
.chat > *{ pointer-events:auto; }
body.lb-open .chat{ display:none; }

.chat-tab{
  display:flex; align-items:center; gap:9px;
  padding:10px 15px;
  border:1px solid var(--glass-line);
  border-radius:999px;
  background:rgba(18,18,22,.86);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 10px 30px rgba(0,0,0,.45);
  color:var(--bone);
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
  cursor:pointer;
  transition:border-color .14s ease, transform .14s ease;
}
.chat-tab:hover{ border-color:var(--line-2); transform:translateY(-1px); }

/* Grey when it is only you. Green, and quietly breathing, when somebody else is in. */
:root{
  --talk:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3C6.9 3 2.8 6.4 2.8 10.6c0 2.4 1.3 4.5 3.4 5.9-.2 1.3-.9 2.6-1.9 3.5 1.8-.2 3.5-.9 4.9-1.9 .9.2 1.9.3 2.8.3 5.1 0 9.2-3.4 9.2-7.8S17.1 3 12 3z'/%3E%3C/svg%3E");
}

.chat-dot{
  /* Absent until somebody is actually there. A grey disc on an empty studio is a light
     that is always on saying nothing, and on the round head it read as a smudge of dirt
     on the rim. Nothing to see, then a green one when it means something. */
  display:none;
  width:8px; height:8px; flex:none; border-radius:50%;
}
.chat-dot.on{
  display:block;
  background:#39d98a;
  box-shadow:0 0 0 0 rgba(57,217,138,.5);
  animation:chat-pulse 2.4s ease-out infinite;
}
@keyframes chat-pulse{
  70%{ box-shadow:0 0 0 7px rgba(57,217,138,0); }
  100%{ box-shadow:0 0 0 0 rgba(57,217,138,0); }
}
@media (prefers-reduced-motion: reduce){ .chat-dot.on{ animation:none; } }

.chat-unread{
  min-width:18px; padding:0 5px;
  border-radius:999px;
  background:#ff5c8a; color:#0b0b0d;
  font:700 10px/18px "Space Mono", monospace;
  text-align:center;
}

.chat-panel{
  width:min(330px, calc(100vw - 36px));
  height:min(440px, calc(100dvh - 120px));
  display:flex; flex-direction:column;
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  background:rgba(18,18,22,.9);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 30px 70px rgba(0,0,0,.55);
  overflow:hidden;
}

.chat-head{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:12px 14px;
  border-bottom:1px solid var(--line);
}
.chat-title{
  margin:0;
  font:400 10px/1.4 "Space Mono", monospace;
  letter-spacing:.16em; text-transform:uppercase; color:var(--dim);
}
.chat-x{
  width:22px; height:22px; flex:none;
  border:0; background:none; cursor:pointer;
  position:relative;
}
.chat-x::before, .chat-x::after{
  content:""; position:absolute; left:4px; top:10px;
  width:14px; height:1px; background:var(--dim);
}
.chat-x::before{ transform:rotate(45deg); }
.chat-x::after{ transform:rotate(-45deg); }
.chat-x:hover::before, .chat-x:hover::after{ background:var(--bone); }

.chat-log{
  flex:1; min-height:0;
  margin:0; padding:12px 14px;
  list-style:none;
  overflow-y:auto;
  display:flex; flex-direction:column; gap:9px;
}
.chat-none{ color:var(--dimmer); font-size:12px; }

.chat-msg{
  max-width:86%;
  align-self:flex-start;
  padding:8px 11px;
  border-radius:12px 12px 12px 3px;
  background:var(--ink-3);
  font-size:13px; line-height:1.45; color:var(--silver);
  /* A message is somebody else's text: it must not be able to stretch the panel. */
  overflow-wrap:anywhere;
}
.chat-msg.is-mine{
  align-self:flex-end;
  border-radius:12px 12px 3px 12px;
  background:rgba(47,139,240,.16);
  color:var(--bone);
}
.chat-from{
  display:block;
  font:400 9px/1.4 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase; color:var(--dim);
  margin-bottom:3px;
}
.chat-body{ display:block; white-space:pre-wrap; }
.chat-at{
  display:block; margin-top:3px;
  font:400 9px/1 "Space Mono", monospace; color:var(--dimmer);
  text-align:right;
}

.chat-emoji{
  display:grid; grid-template-columns:repeat(9, 1fr); gap:2px;
  padding:8px 10px;
  border-top:1px solid var(--line);
  max-height:120px; overflow-y:auto;
}
.chat-e{
  border:0; background:none; cursor:pointer;
  font-size:17px; line-height:1; padding:4px;
  border-radius:6px;
}
.chat-e:hover{ background:var(--ink-3); }

.chat-say{
  display:flex; align-items:flex-end; gap:8px;
  padding:10px;
  border-top:1px solid var(--line);
}
.chat-emo{
  flex:none; width:30px; height:30px;
  border:0; background:none; cursor:pointer;
  font-size:16px; line-height:1; border-radius:8px;
}
.chat-emo:hover{ background:var(--ink-3); }
.chat-say textarea{
  flex:1; min-width:0;
  margin:0; padding:7px 10px;
  border:1px solid var(--line); border-radius:10px;
  background:var(--ink-2); color:var(--bone);
  font:400 13px/1.45 Helvetica, Arial, sans-serif;
  resize:none; max-height:120px;
}
.chat-say textarea:focus{ outline:none; border-color:var(--line-2); }
.chat-send{
  flex:none;
  padding:8px 13px;
  border:1px solid var(--line); border-radius:10px;
  background:var(--bone); color:var(--void);
  font:700 10px/1 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
  cursor:pointer;
}
.chat-send:hover{ background:#fff; }

@media (max-width: 560px){
  .chat{ right:12px; bottom:12px; left:12px; align-items:stretch; }
  .chat-tab{ align-self:flex-end; }
  .chat-panel{ width:auto; height:min(60dvh, 420px); }
  /* Nine across is a squeeze on a narrow phone. */
  .chat-emoji{ grid-template-columns:repeat(7, 1fr); }
}

/* ------------------------------------------------------------------ tag us

   Bottom centre, after a client has downloaded something. Centre rather than a corner
   on purpose: the download itself lands bottom-left in Chrome and bottom-right in
   Safari, and this should not be arriving on top of the thing it is about. */

.tagask{
  position:fixed; z-index:95;
  left:50%; bottom:22px;
  transform:translate(-50%, 14px);
  opacity:0;
  display:flex; align-items:center; gap:12px;
  max-width:calc(100vw - 32px);
  padding:11px 12px 11px 18px;
  border:1px solid var(--glass-line);
  border-radius:999px;
  background:rgba(18,18,22,.9);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 14px 40px rgba(0,0,0,.5);
  transition:opacity .34s ease, transform .34s cubic-bezier(.2,.7,.3,1);
}
.tagask.on{ opacity:1; transform:translate(-50%, 0); }

.tagask-say{
  font-size:13px; line-height:1.4; color:var(--silver);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.tagask-say a{
  color:var(--bone);
  font-weight:700;
  text-decoration:none;
  border-bottom:1px solid rgba(244,244,246,.35);
}
.tagask-say a:hover{ border-bottom-color:var(--bone); }

.tagask-x{
  position:relative;
  width:24px; height:24px; flex:none;
  border:0; border-radius:50%; background:none; cursor:pointer;
}
.tagask-x::before, .tagask-x::after{
  content:""; position:absolute; left:5px; top:11px;
  width:14px; height:1px; background:var(--dim);
}
.tagask-x::before{ transform:rotate(45deg); }
.tagask-x::after{ transform:rotate(-45deg); }
.tagask-x:hover{ background:var(--ink-3); }
.tagask-x:hover::before, .tagask-x:hover::after{ background:var(--bone); }

@media (prefers-reduced-motion: reduce){
  .tagask{ transition:opacity .2s ease; transform:translate(-50%, 0); }
}
@media (max-width: 480px){
  .tagask{ left:16px; right:16px; bottom:16px; transform:translateY(14px); max-width:none; }
  .tagask.on{ transform:translateY(0); }
  .tagask-say{ white-space:normal; }
}

/* ------------------------------------------------------------------ chat gifs

   The picker and its button were removed with Tenor - see the note in 029. What is
   left is the rule for drawing a message that has a gif_url, which costs nothing and
   means a picker could be put back without touching this file. */

/* A sent GIF. The bubble gives up its padding for it - a picture in a speech bubble
   with a 11px cushion round it looks like a mistake. */
.chat-msg:has(.chat-gif){ padding:4px; }
.chat-gif{
  display:block;
  width:100%; max-width:220px;
  border-radius:9px;
  background:var(--ink-3);
}


/* ===========================================================================
   The portal on a phone
   ---------------------------------------------------------------------------
   Everything above this line was written for a screen with a mouse on it and
   then made to survive a narrow one. This block is the other way round: it is
   what the portal is when it has been added to a home screen and is being used
   one-handed, standing up, in a venue.

   Four ideas, and the rest follows from them:

     1. The page runs edge to edge, under the notch and under the home bar, and
        pays for it with env(safe-area-inset-*) padding wherever something would
        otherwise be unreachable or covered.
     2. Navigation lives at the bottom, where a thumb is, not at the top where a
        row of five 10px words was.
     3. Anything that was a floating dialog becomes a sheet that comes up from
        the bottom edge.
     4. Nothing is smaller than a fingertip, and no text field is small enough
        to make Safari zoom the page when it is tapped.
   =========================================================================== */

:root{
  /* Named once so the arithmetic below reads as arithmetic. They are 0px on every
     device without a notch, which is most of them, so these rules cost nothing there. */
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --sar: env(safe-area-inset-right, 0px);
  /* The height of the phone tab bar, excluding the safe area beneath it. */
  --tabh: 58px;
}

/* A phone zooms the whole page when a text field smaller than 16px is tapped, and then
   leaves it zoomed. Nothing about the portal is improved by that. The rule is narrowed
   to coarse pointers so a desktop keeps its 14px fields. */
@media (pointer: coarse){
  input, select, textarea, .field input, .field textarea, .ask select{
    font-size:16px;
  }

  /* Forty-four points is the smallest thing a thumb hits reliably, and the sign-in
     button was thirty-seven. Set as a minimum rather than a height so nothing that is
     already bigger gets squashed. */
  button.go, .act, .act-quiet, .ask-cancel, .who-out, .bulk-act{ min-height:46px; }
  .bell, .menu, .icon-act{ min-width:42px; min-height:42px; }
  /* A text link in a paragraph is still a link; one that is the only way back to sign
     in is a control, and wants the room. */
  .alt button, .auth .linky{ min-height:40px; }
}

html{ -webkit-text-size-adjust:100%; }

/* No grey flash on every tap, and no rubber-band drag of the whole document - both of
   which are the tells that say "this is a web page" rather than "this is an app". */
a, button, [role="button"], label, summary{ -webkit-tap-highlight-color:transparent; }
html, body{ overscroll-behavior-y:none; }

/* ------------------------------------------------------------ the top bar */

.bar{
  /* Under the clock and the battery rather than stopping below them. */
  padding-top:var(--sat);
  padding-left:max(clamp(16px,3.5vw,32px), var(--sal));
  padding-right:max(clamp(16px,3.5vw,32px), var(--sar));
  height:calc(62px + var(--sat));
}

/* The three dots. Everything the bar has no room to say: who is working, notifications,
   add to home screen, and - on a phone - signing out. */
.menu{
  width:38px; height:38px; flex:none;
  display:grid; place-items:center;
  padding:0; cursor:pointer;
  border:1px solid var(--line); border-radius:var(--r-sm);
  background:none; color:var(--dim);
  transition:color .14s ease, border-color .14s ease;
}
.menu svg{ width:19px; height:19px; display:block; }
.menu:hover{ color:var(--bone); border-color:var(--line-2); }
.menu[aria-expanded="true"]{ color:var(--bone); border-color:var(--line-2); }
.menu:focus-visible{ outline:1px solid var(--bone); outline-offset:2px; }

/* ------------------------------------------------------------- the sheet */

/* One element, two shapes. A dropdown hanging off the bar on a desktop; a sheet coming
   up from the bottom edge of a phone. The list inside is identical either way, which is
   the point - there is one menu to keep right, not two. */
.sheet{
  position:fixed; z-index:95;
  top:calc(62px + var(--sat) + 8px); right:16px;
  width:min(320px, calc(100vw - 32px));
  padding:16px 16px 14px;
  border:1px solid var(--glass-line); border-radius:var(--r);
  background:rgba(18,18,22,.86);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 26px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(244,244,246,.06);
  opacity:0; transform:translateY(-8px);
  transition:opacity .16s ease, transform .18s cubic-bezier(.2,.8,.3,1);
}
.sheet.on{ opacity:1; transform:none; }
.sheet[hidden]{ display:none; }

/* Only on a phone, where the sheet is a sheet. A dropdown on a desktop does not dim the
   page behind it - it is a menu, not a mode. */
.sheet-dim{ display:none; }
.sheet-dim[hidden]{ display:none; }

.sheet-grip{ display:none; }
.sheet-who{
  margin:0;
  font:400 14px/1.3 "Space Mono", monospace;
  color:var(--bone);
}
.sheet-sub{
  margin:3px 0 14px;
  font-size:11.5px; color:var(--dimmer);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.sheet-acts{ display:flex; flex-direction:column; gap:2px; }

.sheet-go{
  display:flex; align-items:center; gap:13px;
  width:100%; min-height:48px;
  padding:10px 10px;
  margin:0 -10px;
  border:0; border-radius:var(--r-sm);
  background:none; cursor:pointer;
  color:var(--silver);
  font:400 13px/1.3 "Space Mono", monospace;
  text-align:left;
  transition:background-color .14s ease, color .14s ease;
}
.sheet-go svg{ width:19px; height:19px; flex:none; opacity:.8; }
.sheet-go span{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.sheet-go em{
  font-style:normal; font-size:10.5px; letter-spacing:.06em;
  color:var(--dimmer);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.sheet-go:hover{ background:var(--glass); color:var(--bone); }
.sheet-go:active{ background:var(--glass-2); }

/* A row that is a switch rather than a door. The label keeps the left, the switch takes
   the right, and the whole row is the hit area - a 40px track is a small thing to aim
   at on a phone, and there is nothing else on the row to hit by mistake. */
.sheet-sw span{ flex:1 1 auto; }

.sw{
  position:relative;
  flex:0 0 auto;
  width:42px; height:25px;
  border-radius:999px;
  border:1px solid var(--line);
  background:var(--glass-2);
  transition:background-color .18s ease, border-color .18s ease;
}
.sw::after{
  content:"";
  position:absolute; left:2px; top:50%;
  width:19px; height:19px;
  margin-top:-9.5px;
  border-radius:50%;
  background:var(--silver);
  transition:transform .18s ease, background-color .18s ease;
}

/* Green for on, the same green the portal uses everywhere else for "all well". */
.sheet-sw[aria-checked="true"] .sw{
  background:var(--good);
  border-color:var(--good);
}
.sheet-sw[aria-checked="true"] .sw::after{
  transform:translateX(17px);
  background:var(--void);
}

@media (prefers-reduced-motion:reduce){
  .sw, .sw::after{ transition:none; }
}
.sheet-go.is-out{ color:var(--dim); }
.sheet-go.is-out:hover{ color:var(--bad); }

/* The one word in the menu that is worth colouring: whether this phone will actually
   be told anything. */
.sheet-go em.st-on{ color:var(--good); }
.sheet-go em.st-blocked{ color:var(--bad); }
.sheet-go em.st-ios{ color:#ffd166; }

/* ---------------------------------------------------------------- the toast */

.toast{
  position:fixed; z-index:97;
  left:50%; bottom:calc(26px + var(--sab));
  transform:translate(-50%, 16px);
  margin:0;
  max-width:min(420px, calc(100vw - 32px));
  padding:13px 18px;
  border:1px solid var(--glass-line); border-radius:999px;
  background:rgba(18,18,22,.92);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 14px 40px rgba(0,0,0,.5);
  color:var(--bone);
  font:400 12.5px/1.4 "Space Mono", monospace;
  text-align:center;
  opacity:0; pointer-events:none;
  transition:opacity .2s ease, transform .22s cubic-bezier(.2,.8,.3,1);
}
.toast.on{ opacity:1; transform:translate(-50%, 0); }

/* ------------------------------------------------------------- the tab bar */

/* Hidden by default and shown only on a phone. It is built on every manager's screen
   whatever the width, because a desktop window can be dragged narrow and a tab bar that
   only exists if the page loaded small is a tab bar that is missing half the time. */
.tabs{
  display:none;
  position:fixed; z-index:40;
  left:0; right:0; bottom:0;
  grid-template-columns:repeat(5, 1fr);
  padding-bottom:var(--sab);
  padding-left:var(--sal); padding-right:var(--sar);
  border-top:1px solid var(--glass-line);
  background:rgba(10,10,13,.82);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
}
.tabs a{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:4px;
  min-height:var(--tabh);
  padding:7px 2px 8px;
  text-decoration:none;
  color:var(--dimmer);
  font:400 9px/1 "Space Mono", monospace;
  letter-spacing:.1em; text-transform:uppercase;
  position:relative;
  transition:color .14s ease;
}
.tabs a svg{ width:21px; height:21px; display:block; }
.tabs a.on{ color:var(--bone); }
/* The mark goes above the icon rather than under the label, so it reads at a glance
   without the row growing a second line of furniture. */
.tabs a.on::before{
  content:"";
  position:absolute; top:0; left:50%; transform:translateX(-50%);
  width:26px; height:2px;
  background:var(--line-accent);
}
.tabs a:active{ color:var(--silver); }

/* A photograph at full screen should have the screen. */
body.lb-open .tabs{ display:none; }

/* --------------------------------------------------- the phone layout proper */

@media (max-width: 760px){
  /* The five destinations move to the bottom. Keeping them in the bar as well would be
     the same list twice on the smallest screen there is. */
  .bar-nav{ display:none; }
  .who-name, .who-out{ display:none; }

  /* One row again. The old rule wrapped the bar to three on a phone to fit a name and
     a Sign out button that are now in the menu. */
  .bar{
    height:calc(56px + var(--sat));
    flex-wrap:nowrap;
    gap:12px;
    padding-left:max(16px, var(--sal));
    padding-right:max(16px, var(--sar));
  }
  .bar-mark img{ height:16px; }
  .bar-who{ width:auto; margin-left:auto; gap:8px; }

  .tabs{ display:grid; }

  /* Room for the tab bar, and then some: the last row of a list should not stop dead
     against it. */
  body.has-tabs .page{
    padding-bottom:calc(var(--tabh) + var(--sab) + 28px);
  }
  .page{
    padding-left:max(16px, var(--sal));
    padding-right:max(16px, var(--sar));
  }

  /* The notification panel clears the tab bar rather than ending underneath it. */
  .bell-panel{
    max-height:calc(100dvh - 56px - var(--sat) - var(--tabh) - var(--sab) - 40px);
    overflow:auto;
  }

  /* The sheet becomes a sheet. */
  .sheet{
    top:auto; right:0; left:0; bottom:0;
    width:auto;
    padding:10px 20px calc(18px + var(--sab));
    border-radius:20px 20px 0 0;
    border-left:0; border-right:0; border-bottom:0;
    transform:translateY(100%);
    transition:transform .26s cubic-bezier(.2,.8,.3,1), opacity .18s ease;
  }
  .sheet.on{ transform:translateY(0); }
  .sheet-dim{
    display:block;
    position:fixed; inset:0; z-index:94;
    background:rgba(2,2,4,.62);
    opacity:0;
    transition:opacity .22s ease;
  }
  .sheet-dim.on{ opacity:1; }
  .sheet-dim[hidden]{ display:none; }
  /* The handle every phone sheet has. Not draggable - it is a signal, not a control -
     but its absence is what makes a panel look like it has landed by accident. */
  .sheet-grip{
    display:block;
    width:38px; height:4px;
    margin:0 auto 12px;
    border-radius:999px;
    background:var(--line-2);
  }
  .sheet-who{ font-size:15px; }
  .sheet-go{ min-height:54px; font-size:14px; }

  .toast{ bottom:calc(var(--tabh) + var(--sab) + 18px); }

  /* ---- dialogs are sheets too ---- */
  .ask{
    position:fixed; inset:auto 0 0 0;
    width:100%; max-width:none;
    max-height:92dvh;
    margin:0;
    border-radius:20px 20px 0 0;
    border-left:0; border-right:0; border-bottom:0;
  }
  .ask::before{
    content:"";
    position:absolute; left:50%; top:9px; transform:translateX(-50%);
    width:38px; height:4px; border-radius:999px;
    background:var(--line-2);
  }
  .ask form{ padding:26px 20px calc(22px + var(--sab)); }
  .ask-x{ top:16px; right:10px; }
  /* Save on the right under the thumb, Cancel out of its way, both full height. */
  .ask-row{ gap:12px; }
  .ask-row .go, .ask-cancel{ min-height:50px; }

  /* The crop dialog is already a full-screen layer with a panel floating in the middle
     of it, so it is the PANEL that becomes the sheet - the dialog itself stays where it
     is and simply stops centring what it holds. */
  .crop[open]{ align-items:flex-end; padding:0; }
  .crop-panel{
    width:100%;
    max-height:92dvh;
    border-radius:20px 20px 0 0;
    border-left:0; border-right:0; border-bottom:0;
    padding:18px 18px calc(16px + var(--sab));
  }

  /* ---- the chat ---- */
  /* Clear of the tab bar when it is shut, and nearly the whole screen when it is open.
     A 420px-tall conversation sitting above a row of tabs is not a chat anybody is
     going to use standing up. */
  .chat{ bottom:calc(var(--tabh) + var(--sab) + 12px); }
  .chat.is-open{
    left:0; right:0; bottom:0;
    align-items:stretch;
    gap:0;
  }
  /* ---- the chat, as a Messenger-style head ----

     Shut, it is a circle rather than a bar with STUDIO written across it: a phone has
     one corner to spare and a word does not earn it. The presence dot and the unread
     count hang off the rim, which is where a thumb expects them.

     Open, it is a card with a gutter down both sides rather than a sheet welded to the
     edges of the screen. You can see the gallery behind it, which is the whole point of
     a floating chat - the alternative is leaving the page you were looking at. */
  .chat-tab{
    position:relative;
    /* 44px, down from 54. A Messenger head is a thing you reach for, and at 54 it was
       the largest object on a quiet screen - it read as the page's main action. 44 is
       still a full thumb target and it stops shouting. The warnings bubble opposite is
       sized to match, so the two corners weigh the same. */
    width:44px; height:44px;
    padding:0;
    justify-content:center;
    align-self:flex-end;
    border-radius:50%;
  }
  /* A speech bubble, drawn rather than fetched, the same way the pen and bin are. */
  .chat-tab::before{
    content:"";
    width:19px; height:19px;
    background:var(--bone);
    -webkit-mask:var(--talk) center/contain no-repeat;
    mask:var(--talk) center/contain no-repeat;
  }
  .chat-tab-say{ display:none; }

  .chat-tab .chat-dot{
    position:absolute; right:3px; bottom:3px;
    width:10px; height:10px;
    border:2px solid rgba(18,18,22,.95);
  }
  .chat-tab .chat-unread{
    position:absolute; top:-3px; right:-3px;
  }

  .chat.is-open{
    left:12px; right:12px;
    bottom:calc(var(--tabh) + var(--sab) + 12px);
    align-items:stretch;
    gap:0;
  }
  /* The tab is how you open it; once it is open it is only taking up a line of the
     conversation. */
  .chat.is-open .chat-tab{ display:none; }
  .chat.is-open .chat-panel{
    width:auto; height:min(66dvh, 520px);
    border-radius:18px;
  }

  /* ---- things a finger has to hit ---- */
  .icon-act{ width:42px; height:42px; }
  .icon-act svg{ width:19px; height:19px; }
  .act, .act-quiet{ padding:14px 18px; }
  .bulk-act{ padding:11px 15px; }
  .back{ padding:6px 0; font-size:12.5px; }
  .linky{ padding:4px 0; }
}

/* The lightbox runs under the notch like everything else, so its bar and its controls
   have to come back out from under it. Not inside the width query: a phone held
   sideways is 800-odd points wide and still has the same notch. */
.lb{
  padding-top:var(--sat);
  padding-bottom:var(--sab);
  padding-left:var(--sal);
  padding-right:var(--sar);
}

/* --------------------------------------------------------- once installed */

/* Only when the portal is running from the home screen. There is no browser chrome to
   lean on - no address bar to pull down to refresh, no back gesture that means anything
   - so the few things it did borrow have to be here instead. */
@media (display-mode: standalone){
  /* Selecting the interface by accident is a browser habit. In an app it is just a way
     to make a long press highlight a label instead of opening a menu. Deliberately not
     applied to the places where reading and copying is the point. */
  .bar, .tabs, .sheet, .bulk, .filters, .layouts{
    -webkit-user-select:none; user-select:none;
  }
  /* The bar is the top of the window now, so it is opaque rather than translucent -
     there is nothing above it to show through, and a translucent strip under a status
     bar reads as a rendering fault. */
  .bar{ background:rgba(8,8,10,.94); }
}


/* ---------------------------------------------------------- dragging a frame */

/* A frame can be dragged onto a folder row, or onto the back link to go up a level.
   The Move tool on the tile does the same thing through a dialog and is what works on a
   phone, so none of this is load-bearing - it is the shortcut for eight frames filed in
   the wrong place. */

.tile.is-dragging{
  opacity:.34;
  /* The tile is the drag image. Without this the browser snapshots it mid-transition
     and the ghost under the cursor arrives half faded. */
  transition:none;
}

/* While something is in the air, every place it could land says so. Quiet - a dashed
   edge and a lift in the text, not a colour wash: these rows are a file list first and
   a drop target only for the second or two a drag lasts. */
body.asset-dragging .rows.folders .row-link,
body.asset-dragging .back{
  outline:1px dashed var(--line-2);
  outline-offset:-2px;
  border-radius:var(--r-sm, 6px);
}

/* The one actually under the cursor. */
.rows.folders .row-link.drop-on,
.back.drop-on{
  outline:1px solid var(--blue);
  outline-offset:-2px;
  background:rgba(47,139,240,.12);
  color:var(--bone);
}
.rows.folders .row-link.drop-on .av-folder{
  color:var(--blue);
  border-color:rgba(47,139,240,.45);
}
/* .rows a:hover shifts its padding, which on a drop target reads as the row twitching
   away from the cursor. */
.rows.folders .row-link.drop-on{ padding-left:14px; }

/* Touch does not drag, and a dashed outline that can never be used is noise. */
@media (hover:none){
  body.asset-dragging .rows.folders .row-link,
  body.asset-dragging .back{ outline:0; }
}

/* ---------------------------------------------------------- who has access */

/* The panel that says who outside the studio can see a client, a project or a folder,
   and the dialog bits that go with adding one. Built on .rows and .av, which already
   draw every other list in here - a share is a list of people, and it should not look
   like a different kind of object just because it is newer. */

.access{
  margin:34px 0 0;
  padding-top:22px;
  border-top:1px solid var(--line);
}
.access-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:16px; margin-bottom:4px;
}
.access-head h2{
  margin:0; font-size:13px; letter-spacing:.18em; text-transform:uppercase;
  color:var(--dim); font-weight:400;
}
.access-rows{ margin-top:14px; }

/* QUICK SHARE, now the first block in Manage access.
   .access hangs a rule above itself and 34px of air, which is right for a panel that
   follows something and wrong for one that opens the dialog - it would draw a line
   directly under the subtitle. Flipped: no rule above, the rule below, so it reads as
   the top section of the dialog rather than as something pushed up there. */
#mgr-body > .quick:first-child{
  margin:2px 0 0;
  padding:0 0 20px;
  border-top:0;
  border-bottom:1px solid var(--line);
}
#mgr-body > .quick:first-child .ask-note{ margin:8px 0 0; }
#mgr-body > .quick:first-child .access-rows{ margin-top:12px; }
/* .empty-tight carries 16px of its own on top of the note's bottom margin, which read as
   a missing paragraph rather than as breathing room. */
#mgr-body > .quick:first-child .empty-tight{ margin:12px 0 0; }

/* THE LINK, in something you can select and copy from rather than a line of text. */
.linkbox .linkrow{
  display:flex; align-items:center; gap:8px;
  margin:4px 0 2px;
}
.linkbox .linkrow input{
  flex:1 1 auto; min-width:0;
  /* The token is 43 characters of base64 and the address is long. Smaller and tighter
     than a normal field so the whole thing fits without scrolling on a phone. */
  font-size:12px; letter-spacing:0;
}
.linkbox .lk-copy{
  flex:0 0 auto;
  width:34px; height:34px;
  border:1px solid var(--glass-line); border-radius:8px;
  background:var(--glass-2);
}
.linkbox .lk-copy.lk-done{ color:var(--good); }
.linkbox .ask-row{ justify-content:flex-end; }
.linkbox #lk-note{ margin:0 0 12px; }

/* On a touch screen this is the one thing in the dialog you actually have to hit, and
   a 34px square is a thumb-miss. Everything in the row goes to 46px, which is the size
   the rest of the portal uses where there is no mouse. */
@media (hover:none){
  .linkbox .linkrow{ gap:10px; }
  .linkbox .linkrow input{
    min-height:46px; font-size:13px;
    padding-left:12px; padding-right:12px;
  }
  .linkbox .lk-copy{ width:46px; height:46px; }
  .linkbox .lk-copy svg{ width:19px; height:19px; }
}

/* The heading carries its icon on the left. Scoped to .quick: the other access headings
   are plain words and a chain in front of "WHO CAN SEE THIS" would say nothing. */
.quick .access-head h2{ display:flex; align-items:center; gap:9px; margin:0; }

/* Keeps the heading's type so the panel still reads as a section, and takes a button's
   behaviour so it can be pressed. The border is the part that earns its place: without
   hover to discover it on a phone, a line of small caps that happens to be tappable is
   a line of small caps nobody taps. */
.quick-head{
  display:inline-flex; align-items:center; gap:9px;
  margin:0; padding:9px 14px 9px 12px;
  font:inherit;
  font-size:13px; letter-spacing:.18em; text-transform:uppercase;
  color:var(--silver);
  background:var(--glass);
  border:1px solid var(--glass-line); border-radius:var(--r-sm);
  cursor:pointer;
  transition:color .16s ease, background-color .16s ease, border-color .16s ease;
}
.quick-head:hover{
  color:var(--bone);
  background:var(--glass-2);
  border-color:var(--line-2);
}
.quick-head:active{ transform:translateY(1px); }
.quick-head:focus-visible{ outline:2px solid var(--accent-edge); outline-offset:2px; }
@media (hover:none){ .quick-head{ padding:12px 16px; } }
.quick-ico{
  display:grid; place-items:center;
  width:16px; height:16px;
  color:var(--silver);
  /* The heading is letterspaced small caps; the icon is not, and inherits the gap as
     trailing space without this. */
  letter-spacing:0;
}
.quick-ico svg{ width:16px; height:16px; display:block; }

/* A row that is not a link. .rows styles its children assuming an <a>; this is the same
   shape without the hover and the pointer, because there is nowhere to go. */
.row-static{
  display:flex; align-items:center; gap:14px;
  padding:13px 0; width:100%;
}
.row-static:hover{ background:none; padding-left:0; }

.av-person{
  display:grid; place-items:center;
  font-size:12px; letter-spacing:.04em; color:var(--silver);
  background:var(--glass-2); border:1px solid var(--glass-line);
}

/* Waiting on somebody rather than done, so it reads warmer than a plain tag without
   shouting like an error. */
.tag-wait{ color:var(--dim); border-color:var(--line-2); }

.empty-tight{ margin:16px 0 2px; }

/* A row of tick boxes inside the ask dialog. */
.checks{ display:flex; flex-direction:column; gap:10px; margin-top:4px; }
.check{
  display:flex; align-items:baseline; gap:10px;
  cursor:pointer; line-height:1.4;
}
.check input{
  width:16px; height:16px; flex:none; margin:0;
  accent-color:var(--blue); cursor:pointer;
}
.check span{ color:var(--bone); }
/* The hint goes under the label rather than beside it. Inline, a two-word label and a
   seven-word hint fought for the same line and both wrapped. The indent lines it up
   with the label text, past the box. */
.check{ flex-wrap:wrap; }
.check em{
  color:var(--dimmer); font-style:normal; font-size:12px;
  /* calc, not 100%: a full-width basis plus a 28px indent is 28px wider than the row
     it sits in, and the dialog grew a sideways scrollbar. */
  flex:1 0 calc(100% - 28px); margin:2px 0 0 28px;
}
/* The label shares the line with its box and wraps inside itself. Without a basis of 0
   it asked for the full width of its text, did not get it, and wrapped away under the
   tick instead - which on a phone put "Invite other people to this" below its own
   checkbox, reading like a heading over an empty box. */
.check > span{ flex:1 1 0; min-width:0; }

/* The filter above a pick field's dropdown. Hidden until the list is long enough to
   need one, so a three-project portal never sees it. */
.pickone-find{ margin-bottom:8px; }

/* ------------------------------------------------------------------ the document viewer
   Nearly the whole window, because the thing being read is a contract and a contract in
   a small box is a contract nobody reads. */
.docview{
  width:100%; max-width:none; height:100%; max-height:none;
  margin:0; padding:0;
  border:0; background:none;
}
/* Only while it is open: a plain display:flex here would override the user agent's
   display:none and leave a closed dialog sitting on the page. The flex context is what
   makes the panel's auto margins centre it vertically. */
.docview[open]{ display:flex; }
.docview::backdrop{ background:rgba(3,3,5,.82); }

.dv-panel{
  display:flex; flex-direction:column;
  width:min(1100px, calc(100vw - 32px));
  height:min(92vh, calc(100dvh - 32px));
  margin:auto;
  background:var(--ink-2, #0b0b0e);
  border:1px solid var(--line);
  border-radius:14px;
  overflow:hidden;
  box-shadow:0 24px 70px rgba(0,0,0,.6);
}

.dv-head{
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding:14px 16px;
  border-bottom:1px solid var(--line);
  flex:none;
}
.dv-name{ display:flex; flex-direction:column; gap:3px; min-width:0; }
.dv-name strong{ font-size:14px; color:var(--bone); }
.dv-name em{
  font-style:normal; font-size:11px; color:var(--dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.dv-acts{ display:flex; align-items:center; gap:10px; flex:none; }

/* Its own close rather than the dialog .ask-x, which is absolutely positioned in the
   corner and sat on top of Download once the header was only 375px wide. */
.dv-x{ font-size:22px; line-height:1; }

.dv-body{
  flex:1 1 auto;
  min-height:0;
  background:var(--void);
  display:flex; align-items:center; justify-content:center;
}

/* The PDF viewer the browser already has, in a box on this page rather than in a tab
   somewhere else. */
.dv-frame{ width:100%; height:100%; border:0; background:#1a1a1e; }

.dv-img{ max-width:100%; max-height:100%; object-fit:contain; display:block; }

.dv-text{
  width:100%; height:100%; margin:0; padding:20px 22px;
  overflow:auto;
  font:inherit; font-size:12.5px; line-height:1.65;
  color:var(--bone);
  white-space:pre-wrap; word-break:break-word;
}

.dv-wait{ margin:0; padding:40px; font-size:13px; color:var(--dim); }

.dv-none{
  display:flex; flex-direction:column; align-items:center; gap:18px;
  padding:48px 32px;
  text-align:center;
}
.dv-none p{ margin:0; max-width:46ch; font-size:13px; line-height:1.6; color:var(--dim); }

/* A phone has no room for a gutter around a document. */
@media (max-width:620px){
  .dv-panel{
    width:100vw; height:100dvh;
    border:0; border-radius:0;
  }
  .dv-head{ padding:12px 14px; }
}

/* The warning's own switch. It reads as the status it used to have a separate tag for,
   and it is also the control, so there is one thing on the row saying whether this
   warning is live instead of a label and a button that have to agree.

   Amber rather than the green used everywhere else for "all well": a client online or
   an account activated is good news, and a warning currently on somebody's screen is
   the opposite of that. */
.pill-sw{
  display:inline-flex; align-items:center; gap:7px;
  padding:4px 11px 4px 9px;
  border:1px solid var(--line);
  border-radius:999px;
  background:none;
  color:var(--dim);
  font:inherit;
  font-size:9px; letter-spacing:.14em; text-transform:uppercase;
  cursor:pointer;
  transition:color .16s ease, border-color .16s ease, background-color .16s ease;
}
.pill-sw i{
  width:6px; height:6px; border-radius:50%;
  background:var(--dimmer);
  flex:0 0 auto;
}
.pill-sw:hover{ border-color:var(--line-2); color:var(--bone); }

.pill-sw.on{ color:#e8b23a; border-color:rgba(232,178,58,.4); }
.pill-sw.on i{
  background:#e8b23a;
  box-shadow:0 0 0 0 rgba(232,178,58,.55);
  animation:beat 2.4s ease-out infinite;
}
.pill-sw.on:hover{ border-color:rgba(232,178,58,.75); color:#f0c565; }

/* An icon button in a row of actions. Dim until wanted, like the text links beside it. */
.icoact{
  display:inline-flex; align-items:center; justify-content:center;
  width:28px; height:28px;
  padding:0;
  border:0; border-radius:7px;
  background:none;
  color:var(--dim);
  cursor:pointer;
  transition:color .16s ease, background-color .16s ease;
}
.icoact svg{ width:15px; height:15px; }
.icoact:hover{ color:var(--bone); background:var(--glass-2); }

/* The pencil that opens a person's row. Lit while their panel is open, because with
   the word gone that is the only thing saying which row you are editing. */
.pen-act.on{ color:var(--bone); background:var(--glass-3); }
@media (hover:none){
  .pen-act{ width:42px; height:42px; }
  .pen-act svg{ width:18px; height:18px; }
}

@media (max-width:620px){
  .access-head{ flex-direction:column; align-items:flex-start; gap:10px; }
  .row-static{ flex-wrap:wrap; gap:8px 12px; }
}

/* The studio's notes on a collaborator, under their row rather than beside it - a
   paragraph does not fit in a list row, and squeezing it in would truncate the one
   thing you wrote it down to remember. */
.access-note{
  margin:0 0 12px 54px;
  max-width:62ch;
  font-size:13px; line-height:1.55; color:var(--dim);
  white-space:pre-wrap;
}
@media (max-width:620px){ .access-note{ margin-left:0; } }

/* The one action in an access row that moves things along, so it is the one that is not
   grey. Everything else there - Edit, Access, Remove - is a thing you might do; sending
   the invitation is the thing you came to do. */
.linky-go{ color:var(--blue); }
.linky-go:hover{ color:var(--blue); filter:brightness(1.3); }

/* Who let somebody in, on the studio's panel, and "added by the studio" on the band's.
   Quiet: it is the answer to a question you only ask about one row. */
.row-by{
  display:block;
  margin-top:3px;
  color:var(--dim); font-size:11px; font-style:normal; letter-spacing:.04em;
}
.aside-tight{ margin-top:14px; padding-top:0; border-top:0; }

/* ---------------------------------------------------------------- manage access
   The guest list, behind a button rather than down the bottom of every page. Wider
   than .ask because it holds rows rather than fields, and capped in height so a band
   with a long list scrolls inside the panel instead of pushing the close button off
   the screen. */
.mgr{
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  background:rgba(18,18,22,.72);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 30px 70px rgba(0,0,0,.55), inset 0 1px 0 rgba(244,244,246,.06);
  color:var(--bone);
  padding:0;
  width:min(560px, calc(100vw - 32px));
}
.mgr::backdrop{ background:rgba(2,2,4,.72); backdrop-filter:blur(2px); }
.mgr-panel{ padding:26px; position:relative; }
.mgr-panel h2{ margin:0 0 6px; }
.mgr-panel .ask-note{ margin:0 0 16px; }
.mgr-panel #mgr-body{
  max-height:min(62vh, 560px); overflow:auto;
  margin:0 -6px;
  /* More on the right than the left: that side carries the scrollbar, so content needs
     clearing room there that the left does not. */
  padding:0 14px 0 6px;
}

/* Inside the dialog the panel is the whole content, so it loses the rule and the
   margin that set it apart from a page full of photographs. */
.mgr .access{ margin:0; padding:0; border-top:0; }
.mgr .access-head{ margin-bottom:10px; }

/* How many people, on the button. Quiet - it is a count, not a notification. */
.act-n{
  display:inline-block;
  min-width:17px; margin-left:6px; padding:0 4px;
  border-radius:9px;
  background:rgba(244,244,246,.12);
  font-size:11px; line-height:17px; text-align:center;
}

/* A collaborator's own photograph, in the guest list. .av already sets the box; this
   only has to make the picture fill it, and lose the placeholder's border so a face
   does not sit inside a visible frame. */
.av-person:has(img){ background:none; border-color:transparent; padding:0; overflow:hidden; }
.av-person img{ width:100%; height:100%; object-fit:cover; border-radius:inherit;
                display:block; }

/* What a collaborator sees of themselves, at the top of their own page. Not a form -
   a line saying who the studio thinks they are, with a way to correct it. */
.me-card{
  display:flex; align-items:center; gap:14px;
  margin:4px 0 26px; padding:14px 16px;
  border:1px solid var(--glass-line); border-radius:var(--r);
  background:var(--glass-2);
}
.me-card .av{ width:48px; height:48px; border-radius:50%; }
.me-text{ flex:1 1 auto; min-width:0; }
.me-text strong{ display:block; font-size:15px; }
.me-text em{ display:block; margin-top:2px;
             color:var(--dim); font-size:12px; font-style:normal; }
.me-missing{ color:var(--silver); text-decoration:underline; text-underline-offset:3px; }
@media (max-width:620px){
  .me-card{ flex-wrap:wrap; }
  .me-card .act{ width:100%; }
}

/* The access button sits with the client's own details rather than in the row of page
   actions on the right: "who can see this" belongs beside the account state and the
   invitation line, which are the other two facts about who has been let in. */
.head-share{ margin:14px 0 0; }
.head-share .act{ display:inline-flex; align-items:center; }

/* ---------------------------------------------------------------- the guest list
   Its own layout rather than the generic .row-link grid, which has one slot for a
   sub-line and these need four: what they are, their address, what they can do, and
   how far they have got with signing in. A picture and five facts is a profile, which
   is what the studio and the band both asked to see.

   Flex columns throughout, and that is load-bearing rather than taste: these lines are
   block-level spans written one per line in a template literal, so the newlines between
   them become whitespace text nodes. In a block container each one gets a line box of
   its own and the card falls apart into double spacing. A flex container drops
   whitespace-only children, so the gaps are the gaps that were asked for. */
/* Written as .rows li.who as well, and that matters. These rows live in a <ul class=
   "rows">, where the generic row rule - .rows li:not(:has(a)):has(.av) - lays every
   anchor-less row out as a three column grid, and it outranked a bare .who. So this
   block was dead: the row was a grid, and every child style below it (flex:1 1 200px,
   flex:1 1 100%, margin-left:auto) was inert.
   Nothing looked wrong while the row was shut, because auto-placement happened to drop
   the photograph, the text and the pencil into sensible cells. Opening the pencil was
   what showed it: the edit panel was the fifth child, it landed in the 28px meta
   column, and "invite other people to this" came out one letter per line down a
   column 10 pixels wide. */
.rows li.who,
.who{
  display:flex; flex-wrap:wrap; align-items:flex-start;
  gap:0 14px;
  padding:16px 0;
  border-bottom:1px solid var(--line);
}
.who:last-child{ border-bottom:0; }
.who:first-child{ padding-top:4px; }
/* 4px is right where the list follows a heading. Here it follows the Add someone
   button, and the first person was sitting right under it as though they belonged to
   it. Scoped to #access so the client's card above keeps its own spacing. */
#access .who:first-child{ padding-top:18px; }
.who > .av{ width:46px; height:46px; border-radius:50%; }

.who-text{
  flex:1 1 200px; min-width:0;
  display:flex; flex-direction:column; gap:3px;
}
.who-text > strong{
  font-size:15px; color:var(--bone); font-weight:600;
  /* A name is glued to its state mark by a word joiner, so a long single-word name
     plus the mark has no break opportunity at all. break-word lets that one case
     split rather than push the card wide; ordinary names still wrap at spaces. */
  overflow-wrap:break-word;
}

/* The verification tick. Inline so it wraps with the last word of the name rather than
   stranding itself on a line of its own, and nudged up a touch because an optical
   centre on a disc sits lower than the text baseline suggests.

   Same size and same green as .acc-mark.is-active on purpose: the two helpers draw the
   same tick for the same fact, and inside Manage access they sit a few rows apart, where
   14px of --good next to 16px of #39d98a read as two different badges. */
.tick{
  display:inline-block; vertical-align:-3px;
  width:16px; height:16px; margin-left:7px;
  color:#39d98a;
}
.tick svg{ width:16px; height:16px; display:block; }
.who-role{ color:var(--silver); font-size:12.5px; font-style:normal; }
/* Written as .who a.who-mail rather than .who-mail because the generic row styles
   claim every <a> inside .rows li and lay it out as the row grid - which gave the
   address its own 18px of padding and three times the height it needs. */
.who .who-mail,
.who a.who-mail{
  display:block; padding:0; margin:0;
  color:var(--dim); font-size:12.5px; text-decoration:none;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.who-none{ font-style:italic; }

/* IN A DIALOG, NOTHING GETS CUT OFF.
   The ellipsis on .who-mail is right on a page, where rows sit in a wide column and
   staying one line each keeps the list scannable. In a dialog the column is as narrow
   as the dialog, and the thing being cut is an email address - the one line in the row
   you might actually need to read character for character. So it wraps instead.

   overflow-wrap:anywhere rather than break-word, because an address has no spaces to
   break at and break-word would leave it hanging off the edge anyway. */
#mgr-body .who .who-mail,
#mgr-body .who a.who-mail{
  white-space:normal;
  overflow:visible;
  text-overflow:clip;
  overflow-wrap:anywhere;
}
/* And anything else long enough to reach the edge breaks rather than overflowing -
   a client called by one unbroken 40-character word, a role somebody typed in full. */
#mgr-body .who-can,
#mgr-body .who-role{ overflow-wrap:break-word; }

/* 200px of reserved width is a page measurement. In a dialog it is wider than what is
   left after a 46px photograph and the padding either side, so the row wrapped and the
   photograph ended up sitting alone above the name. The text wraps now, so it does not
   need the basis it used to. */
#mgr-body .who-text{ flex:1 1 140px; overflow-wrap:break-word; }
.who-can{ color:var(--dim); font-size:12px; }
/* The label is dimmer than the thing it labels, so the eye lands on the permission and
   the word "Permissions" is there only if you went looking for it. The colon lives in
   CSS rather than the markup, so no caller can forget it or add a second one. */
.wc-lab{ color:var(--dimmer); font-style:normal; }
.wc-lab::after{ content:": "; }
.who-tags{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:5px; }
.who-tags:empty{ display:none; }
.who-tags .row-by{ margin-top:0; }

/* The studio's private note. Set in from the rest so it reads as an annotation on the
   person rather than another fact about them. */
.who .access-note{
  margin:7px 0 0; padding:0 0 0 10px;
  border-left:1px solid var(--line-2);
  color:var(--dim); font-size:12px; line-height:1.55;
}

/* On their own line, right-aligned, rather than stacked four deep down the side where
   they outweigh the person they belong to. Indented past the photograph so the eye
   reads them as part of this card and not the next one. */
.who-acts{
  flex:1 1 100%;
  display:flex; flex-wrap:wrap; justify-content:flex-end; align-items:center;
  gap:16px;
  margin-top:10px; padding-left:60px;
}
@media (max-width:560px){
  .who-acts{ padding-left:0; justify-content:flex-start; }
}

/* The pencil's own slot, always on the first line against the right edge, whatever else
   the row is carrying. .who-acts keeps the wide things - "Re-send invitation" - which
   still get a line of their own underneath, and collapses when there are none. */
.who-pen{
  flex:0 0 auto;
  align-self:flex-start;
  margin-left:auto;
}
.who-acts:empty{ display:none; }


/* The paper plane sits inside the text link rather than beside it, so the whole thing
   is one target and the icon moves with the words when they wrap. */
.linky-go{ display:inline-flex; align-items:center; gap:7px; }
.linky-go svg{ width:14px; height:14px; flex:0 0 auto; }


/* ---------------------------------------------------------------- the dots menu
   On the card it sits over the picture, top right, and only darkens when wanted. On a
   row it sits before the link. Either way it is outside the anchor, so pressing it
   never navigates. */
.dots{
  position:absolute; top:8px; right:8px; z-index:3;
  width:30px; height:30px;
  display:grid; place-items:center;
  border:0; border-radius:50%;
  background:rgba(8,8,10,.55);
  -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
  color:var(--silver);
  cursor:pointer; opacity:0;
  transition:opacity .14s ease, background-color .14s ease, color .14s ease;
}
.dots svg{ fill:currentColor; display:block; }
.card{ position:relative; }
.card:hover .dots,
.card:focus-within .dots,
.dots:focus-visible{ opacity:1; }
.dots:hover{ background:rgba(8,8,10,.8); color:var(--bone); }
/* No hover on a touch screen, so there it is simply always there. */
@media (hover:none){ .dots{ opacity:1; } }

.rows li{ position:relative; }
.rows li > .dots{ position:static; opacity:1; background:none; width:26px; height:26px;
                  order:9; align-self:center; }
.rows li > .dots:hover{ background:var(--glass-2); }

/* The popover. Fixed, because the cards scroll and an absolutely positioned menu
   inside one would be clipped by the grid. */
.pop{
  position:fixed; z-index:400;
  min-width:184px; padding:6px;
  border:1px solid var(--glass-line); border-radius:var(--r-sm);
  background:rgba(18,18,22,.94);
  -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
  box-shadow:0 18px 44px rgba(0,0,0,.55);
}
.pop-i{
  display:block; width:100%;
  padding:9px 10px;
  border:0; border-radius:6px;
  background:none; color:var(--bone);
  font:inherit; font-size:13px; text-align:left;
  cursor:pointer;
}
.pop-i:hover,
.pop-i:focus-visible{ background:var(--glass-2); outline:none; }
.pop-i.is-bad{ color:var(--dimmer); }
.pop-i.is-bad:hover{ color:var(--bad); }
.pop-rule{ display:block; height:1px; margin:5px 4px; background:var(--line); }

/* ------------------------------------------------- cards line up across the grid
   A card with nothing to say under its name - no description, no presence, no date
   behind its invitation - is one line shorter than its neighbours, so its tag and its
   project count sat a line higher than everybody else's and the row read as ragged.

   Rather than padding the gap with an empty line, the card becomes a column and its
   foot is pushed to the bottom of it. Grid items already stretch to the tallest in the
   row, so the tags land on the same baseline whatever is above them. */
.card{ display:flex; flex-direction:column; height:100%; }
.card > a{ display:flex; flex-direction:column; flex:1 1 auto; min-height:0; }
.card-foot{ margin-top:auto; padding-top:9px; }
/* The name is the one thing that should sit immediately under the photograph; the
   slack belongs below the words, not between them. */
.card-name{ margin-top:11px; }
.card > a > .card-sub:last-child,
.card > a > .card-note:last-child{ margin-bottom:0; }

/* An empty line is still a line. shelf() always prints the sub element, so a client
   with nothing to say under their name was getting a blank block with its own margin -
   which is the 25px that pushed that card's tag out of line with its neighbours. */
.card-sub:empty, .card-note:empty,
.row-sub:empty, .row-note:empty{ display:none; }

/* The last of the ragged-card problem. The foot lays the account tag and the project
   summary out in a wrapping row, so a short tag ("No email yet") sat beside the count
   while a long one ("Invitation opened") pushed it onto a second line - and no two
   cards agreed. In a card there is never enough width for both, so they are always two
   lines, and every card in the row reads at the same height. */
.cards .card-foot .row-side{
  flex-direction:column; align-items:flex-start; gap:6px;
  width:100%;
}
.cards .card-foot .row-state{ flex-direction:column; align-items:flex-start; gap:6px; }

/* On a phone the form rules give every input a 48px minimum so it can be hit with a
   thumb. A checkbox does not need it - the whole label is the target - and with
   align-items:baseline the words were dropping to the bottom of a 48px-tall box and
   sitting well below the tick. Centred, and the box is its own size again. */
.check{ align-items:center; }
.check input[type="checkbox"]{
  width:18px; height:18px; min-height:0; min-width:0; flex:0 0 auto;
}

/* The client's own row, above the guest list. Separated by a rule rather than a
   heading: it is the same kind of row, it just happens to be the person the work is
   addressed to. */
/* The client themselves, told apart from everybody else by being a card rather than a
   row. A rule underneath was doing that job and doing it badly: at a glance the page
   was a list of four people, one of whom happens to own the work. The person the whole
   screen is about should not be the same shape as the people they let in.

   The accent down the left is the brand gradient, used here the way the rest of the
   portal uses it - to mark the thing that matters, once per screen. */
.access-owner{
  position:relative;
  margin:0 0 20px;
  padding:12px 16px 6px 16px;
  border:1px solid var(--glass-line);
  border-radius:var(--r-sm);
  background:var(--glass);
  overflow:hidden;
}
.access-owner::before{
  content:"";
  position:absolute; left:0; top:0; bottom:0;
  width:3px;
  background:var(--line-accent);
}
/* Says what the card is before you read who is in it. Set in the lifted violet so it
   belongs to the accent down the left rather than to the text under it. */
.owner-tag{
  margin:0 0 2px;
  font-size:9px; letter-spacing:.2em; text-transform:uppercase;
  color:var(--accent-edge);
}
.access-owner .who:first-child{ padding-top:8px; }
/* Room above the name. padding-top:0 put the avatar hard against the rule under the
   subheading, so the client's own row read as something that had slipped rather than
   the first entry in a list. */
.access-owner .who{ border-bottom:0; padding-top:14px; }

/* ------------------------------------------------- one person, opened in place
   Four links and three prompts for one person was the complaint, and fairly. Invite
   keeps its own button because it does something irreversible to somebody's inbox;
   everything else - their details, what they can do, and the way out - is behind one
   Edit that opens under the row. */
.who-edit{
  flex:1 1 100%;
  margin:12px 0 2px; padding:16px;
  border:1px solid var(--glass-line); border-radius:var(--r-sm);
  background:var(--glass-2);
}
.who-edit[hidden]{ display:none; }

.we-grid{ display:flex; flex-wrap:wrap; gap:12px; }
.we-f{ flex:1 1 180px; min-width:0; display:flex; flex-direction:column; gap:5px; }
.we-wide{ flex-basis:100%; }
.we-f > span,
.we-lab{
  color:var(--dim); font:400 10px/1.4 "Space Mono", monospace;
  letter-spacing:.14em; text-transform:uppercase;
}
.we-f input{
  width:100%; padding:9px 11px; min-height:0;
  border:1px solid var(--line-2); border-radius:7px;
  background:var(--ink-2, rgba(8,8,10,.5)); color:var(--bone);
  font:inherit; font-size:13.5px;
}
.we-f input:focus{ outline:none; border-color:var(--silver); }
.we-hint{ color:var(--dimmer); font-size:11px; font-style:normal; line-height:1.5; }

.we-perms{ margin-top:14px; display:flex; flex-direction:column; gap:7px; }
.we-perms .checks{ display:flex; flex-direction:column; gap:7px; }

.we-acts{
  display:flex; align-items:center; justify-content:space-between;
  gap:14px; flex-wrap:wrap;
  margin-top:16px; padding-top:13px;
  border-top:1px solid var(--line);
}
.we-right{ display:flex; align-items:center; gap:14px; margin-left:auto; }
.act-sm{ padding:8px 16px; font-size:11px; }

/* The credit request. Quiet, and only ever shown to somebody who is not the studio -
   asking ourselves to tag ourselves would be odd. */
.credit-note{
  display:flex; flex-wrap:wrap; align-items:center; gap:6px;
  /* Room underneath: it is the last thing on the page, and on a phone the tab bar is
     right below it. */
  margin:28px 0 14px; padding-top:18px;
  border-top:1px solid var(--line);
  color:var(--dim); font-size:12.5px;
}
.credit-note a{
  color:var(--silver); text-decoration:underline; text-underline-offset:3px;
}
.credit-note a:hover{ color:var(--bone); }

.lb-credit{
  margin:0; flex:none;
  font:400 11px/1 "Space Mono", monospace;
  letter-spacing:.06em; color:var(--dim);
}
.lb-credit a{ color:var(--silver); text-decoration:none; }
.lb-credit a:hover{ color:var(--bone); text-decoration:underline; text-underline-offset:3px; }
@media (max-width: 760px){ .lb-credit{ display:none; } }

/* The credit request, as a box rather than a line that slides past. Asked once per
   gallery per visit - see askForCredit. Narrow, because it is one sentence and a
   button, and nothing is being decided. */
.credit-pop{
  border:1px solid var(--glass-line);
  border-radius:var(--r);
  background:rgba(18,18,22,.82);
  -webkit-backdrop-filter:var(--blur);
  backdrop-filter:var(--blur);
  box-shadow:0 30px 70px rgba(0,0,0,.55), inset 0 1px 0 rgba(244,244,246,.06);
  color:var(--bone);
  padding:0;
  width:min(400px, calc(100vw - 32px));
}
.credit-pop::backdrop{ background:rgba(2,2,4,.72); backdrop-filter:blur(2px); }
.cp-panel{ padding:28px 26px 24px; text-align:center; }
.cp-panel h2{ margin:0 0 12px; font-size:19px; }
.cp-body{ margin:0 0 22px; color:var(--silver); font-size:14px; line-height:1.6; }
.cp-body a{ color:var(--bone); text-decoration:underline; text-underline-offset:3px; }
.cp-body a:hover{ color:#fff; }
.cp-panel .act{ width:100%; }

/* The message bar, floated. It used to be prepended into #page, which an open lightbox
   covers entirely - so the one moment it most needed to be seen was the one moment it
   could not be. Fixed, centred under the header, and above everything including the
   lightbox and its notes panel. */
.banner-float{
  position:fixed; z-index:120;
  left:50%; top:calc(var(--sat) + 76px);
  transform:translateX(-50%);
  width:min(440px, calc(100vw - 32px));
  margin:0;
  background:rgba(18,18,22,.92);
  -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
  box-shadow:0 18px 44px rgba(0,0,0,.5);
}

/* Who is in the portal right now, above the studio's own presence line. A single quiet
   row - it is a glance, not a list to work from. */
.chat-here{
  /* Shut it is one short line; open it is a column, one row per person. Both shapes
     live in the same pill, so pressing it grows rather than replaces. */
  display:flex; flex-direction:column; align-items:stretch; gap:7px;
  margin:0; padding:9px 14px;
  text-align:left; font:inherit; cursor:pointer;
  transition:border-color .14s ease;
  /* Its own pill, matching the tab below it, because it now floats on the page rather
     than sitting inside a panel that supplied the background. */
  border:1px solid var(--glass-line); border-radius:999px;
  background:rgba(18,18,22,.86);
  -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
  box-shadow:0 10px 30px rgba(0,0,0,.45);
  color:var(--silver); font-size:11.5px; line-height:1.4;
  max-width:min(300px, calc(100vw - 48px));
}
.chat-here .dot{ flex:0 0 auto; }
.chat-here[hidden]{ display:none; }

/* The tab itself carries the unread state, so a count is visible without the panel. */
.chat-tab.has-new{ border-color:rgba(47,139,240,.55); }

/* A live dot on the Manage access button, so somebody being in the portal is visible
   without opening the panel that knows it. */
.act.has-live{ border-color:rgba(70,220,140,.45); }
.act.has-live .dot{ margin-right:7px; vertical-align:middle; }

/* Two phone-only alignment faults, both from touch-target rules.

   .lay becomes 42x42 under (hover:none) so a thumb can hit it, while .pickmode beside
   it stayed 29 - and .head bottom-aligns, so the pair sat visibly off each other. The
   select button matches now rather than the layout buttons shrinking, because the
   bigger one is the right size for a thumb. */
@media (hover: none){
  .pickmode{ min-height:42px; }
}

/* .chat switches to align-items:stretch on a phone, which made the presence pill take
   its whole max-width and float off the right edge instead of hugging its own text.
   It asks for itself rather than relying on the container. */
.chat-here{ align-self:flex-end; }

/* One person in the presence pill: a dot, who they are, and what they are up to. */
.ch-one{ display:block; line-height:1.45; }
.ch-one .dot{ margin-right:6px; vertical-align:baseline; }
.ch-one b{ color:var(--bone); font-weight:600; }
.ch-one em{ display:block; margin-left:14px; color:var(--dim); font-style:normal;
            font-size:10.5px; }

/* Shut, it is a single row rather than a column. */
.chat-here[aria-expanded="false"]{ flex-direction:row; align-items:center; gap:7px; }
.chat-here:hover{ border-color:var(--line-2); }
.chat-here:focus-visible{ outline:2px solid var(--silver); outline-offset:2px; }

/* ---------------------------------------------------------------- warnings
   The studio's own bubble, bottom LEFT, mirroring the chat on the right. An icon and
   nothing else: it is a thing you reach for occasionally, and a word there would be
   furniture on every screen for the rest of time. */
.warn{ position:fixed; left:18px; bottom:18px; z-index:90; }
body.lb-open .warn{ display:none; }
.warn-tab{
  display:grid; place-items:center; position:relative;
  /* The height of the Studio pill beside it, and circular - the two controls read as a
     pair rather than one being bigger because its label is shorter. */
  width:33px; height:33px; padding:0;
  border:1px solid rgba(232,178,58,.4); border-radius:999px;
  background:rgba(18,18,22,.86);
  -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
  /* The neon: a tight ring, a close bloom and a wide soft one, over the usual drop
     shadow so it still sits above the page rather than floating in it. */
  box-shadow:0 0 0 1px rgba(232,178,58,.14),
             0 10px 30px rgba(0,0,0,.45);
  color:#e8b23a; cursor:pointer;
  transition:box-shadow .18s ease, border-color .18s ease, color .18s ease;
}
.warn-tab:hover{
  border-color:rgba(232,178,58,.75);
  box-shadow:0 0 0 1px rgba(232,178,58,.26),
             0 10px 30px rgba(0,0,0,.45);
}
/* Brighter while something is actually showing. */
.warn-tab.on{
  color:#ffc94a; border-color:rgba(232,178,58,.9);
  box-shadow:0 0 0 1px rgba(232,178,58,.35),
             0 10px 30px rgba(0,0,0,.45);
}
.warn-tab svg{ width:16px; height:16px; }
.warn-n{
  position:absolute; top:-3px; right:-3px;
  min-width:15px; height:15px; padding:0 3px;
  border-radius:9px; background:#e8b23a; color:#1a1206;
  font:700 9px/15px "Space Mono", monospace; text-align:center;
}
/* On a phone the two bubbles are a pair, one in each bottom corner, and they are set
   from the same measurements so they sit on one line and read as the same control in
   two colours. 68px was a guess at the tab bar's height and landed two pixels out. */
@media (max-width:760px){
  body.has-tabs .warn{ bottom:calc(var(--tabh) + var(--sab) + 12px); }
  .warn-tab{ width:44px; height:44px; }
  .warn-tab svg{ width:20px; height:20px; }
  /* 44px is already a thumb. The invisible ring below only has to cover the rounding. */
  .warn-tab::after{ inset:-2px; }
}

/* A thumb is not a mouse pointer. The circle stays 33px because it is sized against the
   Studio pill beside it, and an invisible ring takes it to 47px for anybody tapping. */
@media (hover:none){
  .warn-tab::after{
    content:"";
    position:absolute; inset:-7px;
    border-radius:999px;
  }
}

/* ---------------------------------------------------------- back to the top

   Bottom right, under the chat head rather than beside it: two round things in one
   corner, stacked, reading as one column of controls. A client has no chat, so for them
   it simply takes that corner.

   Hidden rather than absent - it fades in once there is something to come back from,
   which keeps it out of the way on every short page in the portal. */

.totop{
  position:fixed; right:18px; bottom:18px; z-index:89;
  display:grid; place-items:center;
  width:36px; height:36px; padding:0;
  border:1px solid var(--glass-line); border-radius:999px;
  background:rgba(18,18,22,.86);
  -webkit-backdrop-filter:var(--blur); backdrop-filter:var(--blur);
  box-shadow:0 10px 30px rgba(0,0,0,.45);
  color:var(--silver); cursor:pointer;
  /* Not display:none - a thing that fades in reads as an offer, where a thing that
     appears reads as something having gone wrong. pointer-events keeps the invisible
     one from catching a tap meant for the page. */
  opacity:0; transform:translateY(8px); pointer-events:none;
  transition:opacity .18s ease, transform .18s ease,
             color .14s ease, border-color .14s ease;
}
.totop svg{ width:17px; height:17px; }
.totop.on{ opacity:1; transform:none; pointer-events:auto; }
.totop:hover{ color:var(--bone); border-color:var(--line-2); }
body.lb-open .totop{ display:none; }
@media (prefers-reduced-motion: reduce){ .totop{ transition:opacity .18s ease; } }

/* Clear of the chat pill, with the same 10px the chat uses between its own parts. */
body.has-chat .totop{ bottom:calc(18px + 33px + 10px); }

@media (max-width:760px){
  /* The same measurements the two bubbles are set from, so all three sit in one column
     above the tab bar. */
  .totop{ width:44px; height:44px; }
  .totop svg{ width:19px; height:19px; }
  body.has-tabs .totop{ bottom:calc(var(--tabh) + var(--sab) + 12px); }
  body.has-tabs.has-chat .totop{ bottom:calc(var(--tabh) + var(--sab) + 12px + 54px); }
}

/* What the reader sees: a line at the very top of the page, above everything. */
.notice{
  display:flex; align-items:flex-start; gap:10px;
  margin:0 0 18px; padding:12px 14px;
  border:1px solid var(--line); border-left-width:2px; border-radius:var(--r-sm);
  font-size:13px; line-height:1.55; color:var(--bone);
}
.notice.is-warn{ border-left-color:#e8b23a; background:rgba(232,178,58,.07); }
.notice.is-info{ border-left-color:var(--silver); background:var(--glass-2); }
.notice-ico{ flex:0 0 auto; color:#e8b23a; line-height:0; }
.notice-ico svg{ width:17px; height:17px; }
.notice.is-info .notice-ico{ display:none; }

/* ---------------------------------------------------------------- dialogs in the middle
   Under 760px every dialog in the portal becomes a sheet along the bottom edge, which is
   right for the long ones - a form you thumb through belongs under your thumb. Quick
   share is two lines and a button, and a two-line sheet stuck to the bottom of an
   otherwise empty screen reads as something that has slipped rather than something that
   has opened.

   Written after that block rather than inside it so it wins on source order without
   needing !important, and scoped to .middle so nothing else moves. */
@media (max-width: 760px){
  .ask.middle{
    position:fixed;
    inset:0;
    width:min(420px, calc(100vw - 32px));
    max-height:88dvh;
    margin:auto;
    border-radius:var(--r);
    border:1px solid var(--glass-line);
  }
  /* The grip says "drag me up from the bottom", and this one is not at the bottom. */
  .ask.middle::before{ content:none; }
  .ask.middle form{ padding:24px 20px 22px; }
  .ask.middle .ask-x{ top:14px; right:8px; }
}

/* The open state of a tile's dots menu, declared last and outside any media query.

   It lived inside @media (hover:none), (max-width:620px) beside the rules that hide the
   tools, and lost - the tools stayed at opacity 0 with the menu "open". Specificity and
   source order are easier to reason about than which of two nested blocks a browser
   resolved first, and tools-on is only ever set by the dots button, which only exists
   on a touch screen, so there is nothing for this to break on a desktop. */
.grid .tile.tools-on .tile-tools{
  opacity:1;
  transform:none;
  pointer-events:auto;
}
