/* ============================================================================
 * Sky — the WeDoFiles house style, layered onto Bulma.
 *
 * Direction 3a from the design docs: deep navy that stays out of the way, one
 * bright cyan that only ever means "this is yours to act on", generous
 * rounding. Figtree carries the language, Chivo Mono carries every number.
 *
 * Bulma 1.x is themed entirely through CSS custom properties, so this file sets
 * variables rather than overriding rules. That matters for upgrades: a Bulma
 * point release can change a selector, but --bulma-primary-h is a contract.
 *
 * Light mode follows the 16a mapping. Hues stay put and only lightness moves,
 * with one exception spelled out there: #35D0EE is 1.8:1 on white and unusable
 * for text, so light mode reads cyan at #0C7C96 and keeps the bright value only
 * for meter fills, where it is a shape rather than a word.
 * ========================================================================== */

:root {
  /* --- Sky palette, dark (the default) --- */
  /* The proposal draws nine neutral steps and this file carried six. The three
   * below are the ones that create separation: a plane behind the ground, a rim
   * that can mean emphasis without borrowing the action colour, and a third
   * text tier. Without them surfaces, borders and text each collapse to one
   * value, which is what reads as flatness. Adjacent steps sit between 1.03 and
   * 1.24 of each other, so every one of them is load-bearing.
   *
   * --sky-faint is the one value here that is NOT the proposal's. It draws
   * #61748A dark and #7D8DA0 light, which measure 3.65 and 3.39 against the
   * surfaces they sit on, where AA needs 4.5 for text at these sizes. These are
   * the nearest values on the same hue and saturation that clear it. The tier,
   * the hue and the role are the proposal's; only the lightness moved, and it
   * moved because the accessibility gate does not bend. */
  --sky-page:    #070B10;  /* the plane behind the ground */
  --sky-deep:    #0A1017;  /* page */
  --sky-panel:   #121A24;  /* cards, panels */
  --sky-panel-2: #16202B;  /* nested surfaces, table headers */
  --sky-edge:    #1F2C3A;  /* the 1px rim that stands in for elevation */
  --sky-edge-2:  #2A3B4D;  /* the same rim, meaning emphasis */
  --sky-bright:  #E8F0F7;  /* body text */
  --sky-muted:   #8FA3B5;  /* secondary text */
  --sky-faint:   #75889E;  /* tertiary text — 4.52 on panel-2, its worst surface */

  /* The select's chevron. This is the one icon in the app that is a
   * background image rather than the icon component: a <select> cannot
   * contain markup, so there is nothing to put an <svg> inside. The geometry
   * is Tabler's chevron-down at the 1.75 stroke the icons plan sets, so the
   * two chevrons on a sorted file list agree.
   *
   * It is a token because a data URI cannot read currentColor, so the glyph
   * needs one value per theme — and a token is where a per-theme value goes
   * in this file. */
  --chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238FA3B5' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6l6 -6'/%3E%3C/svg%3E");

  --sky-cyan:      #35D0EE; /* action, and only action */
  --sky-cyan-text: #35D0EE; /* readable cyan — diverges from the fill in light */
  --sky-cyan-fill: #35D0EE; /* meter fills */
  --sky-action-ink: #0A1017;
  --sky-action-hover: #68DDF2;
  --sky-go:        #3EDC9B;
  --sky-warn:      #F5B944;
  --sky-stop:      #FF6A6A;
  --sky-acted:     #7E8CFF; /* "we acted on this" — distinct from stop */

  /* Space: 4 · 8 · 12 · 20 · 32 · 52 */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px;
  --sp-4: 20px; --sp-5: 32px; --sp-6: 52px;

  /* Radius: 9px on controls and rows, 16px on panels, pill on primary buttons */
  --r-control: 9px;
  --r-panel:   16px;
  --r-pill:    999px;

  /* Control metrics. min-height owns a control's height; padding-block is
   * always small enough that it cannot exceed it. That is what makes a button
   * and the field beside it the same height regardless of their font sizes —
   * the defect this layer exists to close.
   *
   * Radius is assigned by role and the difference is deliberate: pill for
   * buttons, chips and pills; --r-control for text fields, selects and icon
   * buttons. The proposal draws it that way (ui-redesign-proposals.html:219,
   * :228). Do not unify them. */
  --control-h:       40px;
  --control-h-sm:    32px;
  --control-h-touch: 44px;

  /* The mobile tab bar's own height: 8px padding + a 44px row + 8px padding,
   * from the .tabbar rules. Two things clear this bar and they used to say so
   * with unrelated numbers — the selection bar derived 60px, the main column
   * used a flat 84px that accounted for nothing, so on a notched phone the
   * two disagreed by the height of the home indicator. Both derive from this
   * now, and both add env(safe-area-inset-bottom) themselves.
   *
   * The visual footprint is one pixel more than this: .tabbar carries a 1px
   * top border. The selection bar's measured 11px clearance is against the
   * footprint, so it reads as 12px of offset minus that border. */
  --tabbar-h:        60px;

  /* Type. Seven steps, each carrying its size, line-height, weight and
   * tracking, because a rule that can reach the size but not the line-height
   * invents one — which is how eleven line-heights grew out of a scale that
   * specifies seven.
   *
   * The scale was classes before this, and a class is not something .tabbar a
   * or .pill can reference, so every component that needed a size hardcoded
   * one. That is the same failure --control-h fixed for heights, fixed the
   * same way. The .t-* classes stay: they are how a template asks for a step.
   * These tokens are how a component rule does. */
  --t-display-size: 46px;   --t-display-line: 1.02; --t-display-weight: 700;
  --t-display-track: -0.02em;
  --t-h1-size:      32px;   --t-h1-line:      1.1;  --t-h1-weight:      700;
  --t-h1-track:     -0.015em;
  --t-h2-size:      22px;   --t-h2-line:      1.25; --t-h2-weight:      600;
  --t-h2-track:     -0.01em;
  --t-body-size:    16px;   --t-body-line:    1.6;  --t-body-weight:    400;
  --t-ui-size:      14.5px; --t-ui-line:      1.65; --t-ui-weight:      600;
  --t-small-size:   13px;   --t-small-line:   1.5;  --t-small-weight:   400;
  /* The one step the scale was missing. It jumped 13 -> 10 with nothing
   * between, so six declarations invented their own 11px: the column heads,
   * the sort indicator, the brand mark, the tab bar's labels, and the file
   * row's size and date. Small chrome, set tight, because every one of them
   * is a single line. Family stays with the rule — some of these are mono and
   * some are not — the way .t-small and .t-label each pick their own. */
  --t-meta-size:    11px;   --t-meta-line:    1;    --t-meta-weight:    600;

  /* Not steps — a step is a size, and 7.2 added the only one this scale was
   * missing. These are the other two type properties a component rule needs
   * and would otherwise type in: the flat line-height every single-line
   * control uses, and the tracking that goes with an uppercase mono label.
   * Three tracking values rather than one because they are set against three
   * different sizes; widening tracking as size falls is why they differ. */
  --t-line-flat:    1;
  --t-line-snug:    1.4;   /* compact UI text: labels, fields, row names */
  --t-weight-strong: 700;  /* emphasis inside a step, not a step of its own */
  --t-track-head:   0.09em;  /* column heads, 11px */
  --t-track-eyebrow: 0.12em; /* brand mark and legal eyebrows, 10-11px */
  --t-track-chip:   0.02em;  /* the type chip's extension label, 7-9px */
  --t-label-size:   10px;   --t-label-line:   1;    --t-label-weight:   500;
  --t-label-track:  0.16em;

  --font-ui:   "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "Chivo Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Elevation is a 1px lighter rim, never a drop shadow. The upload target is
   * the single exception the design allows, and it uses --shadow-lift below. */
  --shadow-lift: 0 8px 32px rgba(0, 0, 0, 0.45);

  /* Motion curves, §08. One standard curve for interface motion; the second
   * belongs to the sheet slide alone, borrowed from platform sheets — it
   * decelerates hard at the end so the sheet feels like it has weight. */
  --sky-ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --sky-ease-sheet: cubic-bezier(0.32, 0.72, 0, 1);

  color-scheme: dark;
}

:root[data-theme="light"] {
  --sky-page:    #E9EEF4;
  --sky-deep:    #F4F7FA;
  --sky-panel:   #FFFFFF;
  --sky-panel-2: #F4F7FA;
  --sky-edge:    #DCE4EC;
  --sky-edge-2:  #C4D0DC;
  --sky-bright:  #0E1720;
  --sky-muted:   #5A6B7D;
  --sky-faint:   #5E6D80;  /* 4.53 on the page, its worst surface */
  --chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235A6B7D' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6l6 -6'/%3E%3C/svg%3E");

  --sky-cyan:       #0C7C96;
  --sky-cyan-text:  #0C7C96;
  --sky-cyan-fill:  #12A5C4;
  --sky-action-ink: #FFFFFF;
  --sky-action-hover: #09677C;
  --sky-go:         #0F7A57;
  --sky-warn:       #9A6B08;
  --sky-stop:       #B32D2D;
  --sky-acted:      #4A50B8;

  --shadow-lift: 0 8px 24px rgba(14, 23, 32, 0.12);
  color-scheme: light;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --sky-page:    #E9EEF4;
    --sky-deep:    #F4F7FA;
    --sky-panel:   #FFFFFF;
    --sky-panel-2: #F4F7FA;
    --sky-edge:    #DCE4EC;
    --sky-edge-2:  #C4D0DC;
    --sky-bright:  #0E1720;
    --sky-muted:   #5A6B7D;
    --sky-faint:   #5E6D80;  /* 4.53 on the page, its worst surface */
    --chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235A6B7D' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6l6 -6'/%3E%3C/svg%3E");

    --sky-cyan:       #0C7C96;
    --sky-cyan-text:  #0C7C96;
    --sky-cyan-fill:  #12A5C4;
    --sky-action-ink: #FFFFFF;
    --sky-action-hover: #09677C;
    --sky-go:         #0F7A57;
    --sky-warn:       #9A6B08;
    --sky-stop:       #B32D2D;
    --sky-acted:      #4A50B8;

    --shadow-lift: 0 8px 24px rgba(14, 23, 32, 0.12);
    color-scheme: light;
  }
}

/* ---------------------------------------------------------------------------
 * Bulma variable remapping.
 * Bulma derives most of its palette from h/s/l triples, so setting those gives
 * consistent hover, active, and inverted variants for free.
 * ------------------------------------------------------------------------ */
:root {
  --bulma-family-primary: var(--font-ui);
  --bulma-family-code: var(--font-mono);
  --bulma-family-monospace: var(--font-mono);

  --bulma-scheme-h: 210;
  --bulma-scheme-s: 40%;
  --bulma-light-l: 92%;
  --bulma-dark-l: 8%;

  --bulma-primary-h: 190;
  --bulma-primary-s: 84%;
  --bulma-primary-l: 57%;

  --bulma-link-h: 190;
  --bulma-link-s: 84%;
  --bulma-link-l: 57%;

  --bulma-success-h: 158;
  --bulma-success-s: 70%;
  --bulma-success-l: 55%;

  --bulma-warning-h: 40;
  --bulma-warning-s: 90%;
  --bulma-warning-l: 62%;

  --bulma-danger-h: 0;
  --bulma-danger-s: 100%;
  --bulma-danger-l: 71%;

  --bulma-body-background-color: var(--sky-page);
  --bulma-body-color: var(--sky-bright);
  --bulma-text: var(--sky-bright);
  --bulma-text-strong: var(--sky-bright);
  --bulma-text-weak: var(--sky-muted);
  --bulma-border: var(--sky-edge);
  --bulma-border-weak: var(--sky-edge);

  --bulma-radius-small: 6px;
  --bulma-radius: var(--r-control);
  --bulma-radius-large: var(--r-panel);
  --bulma-radius-rounded: var(--r-pill);

  --bulma-link-text: var(--sky-cyan-text);
  --bulma-link-text-hover: var(--sky-cyan-text);
}

/* ---------------------------------------------------------------------------
 * Type scale: 46/1.02 · 32/1.1 · 22/1.25 · 16/1.6 · 14.5/1.65 · 13/1.5 ·
 * 10/1 caps .16em
 * ------------------------------------------------------------------------ */
html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--sky-page);
  color: var(--sky-bright);
  font-family: var(--font-ui);
  font-size: var(--t-body-size);
  line-height: var(--t-body-line);
  font-weight: var(--t-body-weight);
}

[hidden] { display: none !important; }

.t-display { font: var(--t-display-weight) var(--t-display-size)/var(--t-display-line) var(--font-ui); letter-spacing: var(--t-display-track); }
.t-h1      { font: var(--t-h1-weight) var(--t-h1-size)/var(--t-h1-line) var(--font-ui); letter-spacing: var(--t-h1-track); }
.t-h2      { font: var(--t-h2-weight) var(--t-h2-size)/var(--t-h2-line) var(--font-ui); letter-spacing: var(--t-h2-track); }
.t-body    { font: var(--t-body-weight) var(--t-body-size)/var(--t-body-line) var(--font-ui); }
.t-ui      { font: var(--t-ui-weight) var(--t-ui-size)/var(--t-ui-line) var(--font-ui); }
.t-small   { font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui); }
.t-meta    { font: var(--t-meta-weight) var(--t-meta-size)/var(--t-meta-line) var(--font-ui); }
.t-label {
  font: var(--t-label-weight) var(--t-label-size)/var(--t-label-line) var(--font-mono);
  letter-spacing: var(--t-label-track);
  text-transform: uppercase;
  color: var(--sky-faint);
}

/* Every size, ID, price, date and percentage is mono. Fixed width means a
 * number the page re-polls every 3 seconds never nudges the layout — which is
 * why the upload and scan screens do not jitter while they update. */
.num, .mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

@media (max-width: 768px) {
  /* The two display steps step down on a phone. Promoted through the token,
   * the way --control-h promotes to the tap floor, so the .t-* classes stay
   * pure consumers and a component rule reading --t-h1-size gets the right
   * answer at both widths instead of only the desktop one. */
  :root {
    --t-display-size: 34px;
    --t-h1-size: 26px;
  }
}

/* ---------------------------------------------------------------------------
 * Surfaces
 * ------------------------------------------------------------------------ */
.panel-sky {
  background: var(--sky-panel);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
}

.panel-sky__head {
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--sky-edge);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.panel-sky__body { padding: var(--sp-4) var(--sp-5); }

.rule { height: 1px; background: var(--sky-edge); border: 0; margin: var(--sp-4) 0; }

/* ---------------------------------------------------------------------------
 * The signature element: a pill meter with a ghost.
 *
 * The second, translucent fill shows what a pending action *would* add, so the
 * consequence is visible before the server does anything. It is used on the
 * dashboard, in the upload queue, on plan comparison, and on the add-on stepper
 * — one component, four places, which is what makes it a signature rather than
 * a decoration.
 * ------------------------------------------------------------------------ */
.meter {
  --meter-fill: var(--sky-cyan-fill);
  display: block;
}

.meter__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
}

.meter__value { font: var(--t-h2-weight) var(--t-h2-size)/var(--t-h2-line) var(--font-mono); color: var(--sky-bright); }
.meter__of    { font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui); color: var(--sky-faint); }

/* The ghost's own number, e.g. "7.4 + 5.1 / 250 GB" */
.meter__ghost-value { color: var(--meter-fill); }

.meter__track {
  position: relative;
  height: 10px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--sky-edge) 70%, transparent);
  overflow: hidden;
}

.meter__fill {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: var(--r-pill);
  background: var(--meter-fill);
  /* A small non-zero minimum so 3% of 250 GB is still a visible shape rather
   * than a hairline the eye reads as empty (design note in 8b). */
  min-width: 6px;
  transition: width 240ms ease;
}

.meter__ghost {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--meter-fill) 34%, transparent);
  /* The ghost is the one element whose motion carries meaning: queueing an
   * upload extends it before a byte is sent, and the growth is what makes it
   * read as a consequence rather than as a second number (§08 — 420ms). */
  transition: width 420ms var(--sky-ease), left 420ms var(--sky-ease);
}

.meter__foot {
  margin-top: var(--sp-2);
  font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui);
  color: var(--sky-faint);
}

.meter--warn { --meter-fill: var(--sky-warn); }
.meter--over { --meter-fill: var(--sky-stop); }
.meter--go   { --meter-fill: var(--sky-go); }

/* ---------------------------------------------------------------------------
 * State pills.
 *
 * Quarantined is solid and red-rimmed — a sealed thing we stopped from reaching
 * you. Disabled is dashed, unfilled and paired with a struck-through name — a
 * thing withdrawn, with an appeal. Different colour AND different shape, so the
 * distinction survives greyscale and colour-blindness both.
 * ------------------------------------------------------------------------ */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font: var(--t-label-weight) var(--t-meta-size)/var(--t-line-snug) var(--font-mono);
  white-space: nowrap;
}
/* The proposal draws a status dot on every pill at every width
 * (ui-redesign-proposals.html:262). currentColor means each state's own
 * meaning colours its own dot, so the dot needs no modifier rules — and it is
 * why the 5px gap is 5px: it matches the dot's diameter. */
.pill::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

.pill--available   { color: var(--sky-go);    border-color: color-mix(in srgb, var(--sky-go) 40%, transparent);
                     background: color-mix(in srgb, var(--sky-go) 12%, transparent); }
.pill--uploading,
.pill--scanning    { color: var(--sky-cyan-text); border-color: color-mix(in srgb, var(--sky-cyan) 40%, transparent);
                     background: color-mix(in srgb, var(--sky-cyan) 12%, transparent); }
.pill--quarantined { color: var(--sky-deep);  border-color: var(--sky-stop);
                     background: var(--sky-stop); font-weight: var(--t-weight-strong); }
.pill--disabled    { color: var(--sky-acted); border-color: var(--sky-acted);
                     border-style: dashed; background: transparent; }
.pill--error       { color: var(--sky-warn);  border-color: color-mix(in srgb, var(--sky-warn) 45%, transparent);
                     background: color-mix(in srgb, var(--sky-warn) 12%, transparent); }

/* Paired with .pill--disabled: the name is struck because the thing itself was
 * withdrawn, not merely flagged. */
.name--withdrawn { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--sky-muted); }

/* ---------------------------------------------------------------------------
 * Controls
 * ------------------------------------------------------------------------ */
.btn {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--control-h);
  padding-block: 0;
  padding-inline: var(--sp-4);
  border-radius: var(--r-pill);
  border: 1px solid var(--sky-edge-2);
  background: transparent;
  color: var(--sky-bright);
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-flat) var(--font-ui);
  cursor: pointer;
  text-decoration: none;
  transition: background 140ms ease, border-color 140ms ease, opacity 140ms ease;
}
.btn:hover,
.btn.is-hover { border-color: color-mix(in srgb, var(--sky-cyan) 50%, var(--sky-edge)); color: var(--sky-bright); }
.btn:focus-visible { outline: 2px solid var(--sky-cyan); outline-offset: 2px; }

.btn--primary {
  background: var(--sky-cyan);
  border-color: var(--sky-cyan);
  color: var(--sky-action-ink);
}
.btn--primary:hover,
.btn--primary.is-hover { 
  background: var(--sky-action-hover);
  border-color: var(--sky-action-hover);
  color: color-mix(
    in srgb,
    var(--sky-action-ink) 85%,
    light-dark(#0A1017, #FFFFFF)
  );
}

.btn--danger { border-color: var(--sky-stop); color: var(--sky-stop); }
.btn--danger:hover { background: color-mix(in srgb, var(--sky-stop) 12%, transparent); }

.btn--quiet { border-color: transparent; color: var(--sky-muted); padding-inline: var(--sp-3); }
.btn--quiet:hover { color: var(--sky-bright); border-color: var(--sky-edge-2); }

.btn--small {
  min-height: var(--control-h-sm);
  padding-inline: var(--sp-3);
  font-size: var(--t-small-size);
}

.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; }

/* Pending state. The design is explicit that nothing is optimistic: the same
 * verb goes to present tense and the control stops accepting input until the
 * server answers. */
.btn[aria-busy="true"] { opacity: 0.6; pointer-events: none; }

/* ---------------------------------------------------------------------------
 * Icons (§03, §04, §06).
 *
 * One sprite, referenced by <use>. Sized in em so an icon tracks the type size
 * of the control it sits in rather than needing a size argument — which is what
 * keeps @Icon a one-parameter component.
 *
 * stroke-width lives here and nowhere else. The generated symbols deliberately
 * omit it so this value reaches through the <use> shadow tree; 1.75 is the
 * middle of the 1.7–1.9 the proposal draws, against Tabler's own default of 2.
 * ------------------------------------------------------------------------ */
.icon {
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
  stroke-width: 1.75;
  vertical-align: -0.125em;
  pointer-events: none;
}

/* The square icon-only control the proposal draws (.mk-iconbtn, .mph-iconbtn).
 * Height comes from the control token like every other control; width matches
 * it, so the target is always square and always the tap floor on a phone. */
.iconbtn {
  box-sizing: border-box;
  display: inline-grid;
  place-items: center;
  min-height: var(--control-h);
  width: var(--control-h);
  padding: 0;
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: transparent;
  color: var(--sky-muted);
  cursor: pointer;
  font-size: var(--t-body-size);
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
/* Hover lifts to the interactive rim, never to cyan. The colour plan's Phase 4
 * settled that: a hover border says "the pointer is here", which is not an
 * action, and .btn--quiet:hover is the rule this matches. */
.iconbtn:hover { color: var(--sky-bright); border-color: var(--sky-edge-2); }
.iconbtn:focus-visible { outline: 2px solid var(--sky-cyan); outline-offset: 2px; }

/* The row menu's glyph turns with the row. Wide rows put the dots along the
 * row's own axis; at 768 the row becomes two lines and the menu becomes a
 * square at the end of them, where the upright glyph reads as "this column has
 * more". Both are in the DOM and CSS chooses — the one place in the icons plan
 * a glyph is swapped by width, and it swaps no markup, so what HTMX renders is
 * identical at every size. */
/* The row's menu button is a SMALL control, not a default one. Everything
 * beside it in .filerow__actions is .btn--small (--control-h-sm, 32px), and
 * .iconbtn's own min-height is --control-h (40px) — which is 8px taller than
 * the cell that was setting the row's height, and pushed the measured 54.99px
 * desktop row to 58. The row height is a settled figure, so the button matches
 * its neighbours rather than the row growing to fit it.
 *
 * Nothing special happens at 768: --control-h-sm promotes to the 44px tap
 * floor there like every other control token, which is where the 44x44 the
 * phone measures comes from. */
.filerow__actions .action-menu__button {
  min-height: var(--control-h-sm);
  width: var(--control-h-sm);
}

.action-menu__glyph-tall { display: none; }
@media (max-width: 768px) {
  .action-menu__glyph-wide { display: none; }
  .action-menu__glyph-tall { display: inline-grid; }
}

/* Fields ------------------------------------------------------------------ */
.field-sky { margin-bottom: var(--sp-4); }
.field-sky__label {
  display: block;
  margin-bottom: var(--sp-2);
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-snug) var(--font-ui);
  color: var(--sky-bright);
}
.field-sky__hint { margin-top: var(--sp-1); font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui); color: var(--sky-faint); }
.field-sky__error {
  margin-top: var(--sp-1);
  font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui);
  color: var(--sky-stop);
}

.input-sky {
  box-sizing: border-box;
  width: 100%;
  min-height: var(--control-h);
  padding-block: var(--sp-1);
  padding-inline: var(--sp-3);
  border-radius: var(--r-control);
  border: 1px solid var(--sky-edge);
  background: var(--sky-deep);
  color: var(--sky-bright);
  font: var(--t-body-weight) var(--t-body-size)/var(--t-line-snug) var(--font-ui); /* --t-body-size is 16px so iOS Safari does not zoom on focus */
}
.input-sky:focus {
  outline: none;
  border-color: var(--sky-cyan);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sky-cyan) 22%, transparent);
}
.input-sky[aria-invalid="true"] { border-color: var(--sky-stop); }
.input-sky::placeholder { color: color-mix(in srgb, var(--sky-muted) 70%, transparent); }
.input-sky:disabled, .input-sky[readonly] { opacity: 0.6; }

/* A field with an icon in it. The wrapper is the .input-sky now — it carries
 * the border, the background and the height — and the real <input> inside it
 * is stripped back to nothing so there is only one box on screen.
 *
 * The icon is a SIBLING of the input, not a background image: an <input> has
 * no children to put one in, and a background would not inherit currentColor
 * per theme the way the sprite does. Everything the form needs (id, name,
 * type, value, placeholder, the sr-only label) stays on the input itself, so
 * the field submits identically with scripting off. */
.input-sky--with-icon {
  position: relative;
  display: flex;
  align-items: center;
  color: var(--sky-muted); /* the icon; the input sets its own text colour */
}
/* The icon is taken out of flow and the input is padded past it, rather than
 * the two sitting side by side in a flex row. That is what makes the icon area
 * focus the field: the input's own box now extends underneath the glyph, and
 * .icon is pointer-events:none, so a click there lands on the input. Side by
 * side, the same click lands on this wrapper — a <span>, which focuses
 * nothing. No label and no script is involved, so it behaves identically with
 * scripting off. */
.input-sky--with-icon > .icon {
  position: absolute;
  left: var(--sp-3);
  top: 50%;
  transform: translateY(-50%);
}
/* :focus-within, not :focus — the wrapper never receives focus itself, so the
 * ring has to come from the input inside it. */
.input-sky--with-icon:focus-within {
  outline: none;
  border-color: var(--sky-cyan);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sky-cyan) 22%, transparent);
}
.input-sky--with-icon > input {
  border: 0;
  background: none;
  min-height: auto;
  /* Left padding clears the absolutely-positioned glyph: its 1em box plus the
   * gap it would have had in flow. */
  padding: 0 0 0 calc(1em + var(--sp-2));
  width: 100%;
  color: var(--sky-bright);
  font: inherit;
}
.input-sky--with-icon > input:focus { outline: none; }
.input-sky--with-icon > input::placeholder { color: color-mix(in srgb, var(--sky-muted) 70%, transparent); }

/* Native control furniture --------------------------------------------------
 *
 * .input-sky was written for a text field and is applied to controls that draw
 * their own furniture. Each rule below makes room for something the browser was
 * already drawing, rather than removing it: the arrow a select has always had,
 * the marker a disclosure has always had. Nothing here removes a function —
 * every select still opens, every disclosure still toggles.
 * ------------------------------------------------------------------------ */

/* The arrow the browser drew sat inside the same 12px inset as the text, hard
 * against a 9px corner. appearance: none takes the browser's arrow away and the
 * chevron replaces it, with padding-inline-end reserving the room it needs. */
select.input-sky {
  appearance: none;
  padding-inline-end: var(--sp-5);
  background-image: var(--chevron-down);
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
  background-size: 16px 16px;
}
/* A select with no arrow reads as a text field, so the disabled one keeps its
 * chevron and dims with the rest of the control. */
select.input-sky:disabled { opacity: 0.6; }

/* The file input's button is the browser's, and it is the one piece of control
 * furniture that looks nothing like the app around it. ::file-selector-button
 * is the standard way to reach it; it takes the small button's metrics rather
 * than a second set, so it stays right when those change. */
input[type="file"].input-sky { padding-block: var(--sp-1); }
input[type="file"]::file-selector-button {
  margin-inline-end: var(--sp-3);
  min-height: var(--control-h-sm);
  padding-inline: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--sky-bright);
  font: var(--t-ui-weight) var(--t-small-size)/var(--t-line-flat) var(--font-ui);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
input[type="file"]::file-selector-button:hover {
  border-color: color-mix(in srgb, var(--sky-cyan) 50%, var(--sky-edge));
}

/* Horizontal resizing has never been the useful axis, and a textarea dragged
 * wider than its column takes the layout with it. */
textarea.input-sky { resize: vertical; }

/* Ten of the app's fourteen checkboxes had no rule at all: 13px, and whatever
 * accent colour the browser felt like. The two that did have one said it in
 * their own context, which is why nine of them never got it. Radio is here for
 * the same reason in advance — there is one in the plan picker, and it was
 * missed by the inventory that went looking for it. */
/* 24px, not the 18px the two styled contexts used to set. WCAG 2.2 asks for a
 * 24x24 pointer target, and Lighthouse fails a page whose boxes are smaller and
 * close together — which the share panel's permission list is. Measured: 18px
 * scored 97 on the guide page, 24px scores 100. */
input[type="checkbox"],
input[type="radio"] {
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  accent-color: var(--sky-cyan);
}

/* The marker is furniture too, and it is the one piece here that cannot simply
 * be removed: three summaries in the app carry only a type class, and the
 * browser's triangle is the only thing telling a reader they open. So the
 * triangle goes and the same chevron the select uses takes its place, rotated
 * when open.
 *
 * The four families that lay out their own head opt out by name rather than by
 * a selector guessing at them: .record__head and .panel-sky__head are flex rows
 * with their own right-hand content, and the measured file-detail layout runs
 * through the first of them. */
summary {
  cursor: pointer;
  list-style: none;
}
summary::-webkit-details-marker { display: none; }
summary::after {
  content: "";
  float: right;
  width: 16px;
  height: 16px;
  background-image: var(--chevron-down);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 16px 16px;
  transition: transform 140ms var(--sky-ease);
}
details[open] > summary::after { transform: rotate(180deg); }
.record__head::after,
.panel-sky__head::after,
.plan-section__summary::after,
summary.btn::after { content: none; }

/* An ID or a URL next to a copy button. */
.copyfield {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-deep);
}
.copyfield__value {
  flex: 1;
  min-width: 0;
  font: var(--t-small-weight) var(--t-small-size)/var(--t-line-snug) var(--font-mono);
  color: var(--sky-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * Notices — the banner language the design uses for payment failure, capacity,
 * and moderation. Each carries its own accent; none of them use cyan, which is
 * reserved for things the reader can act on.
 * ------------------------------------------------------------------------ */
.notice {
  --notice-accent: var(--sky-muted);
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid color-mix(in srgb, var(--notice-accent) 40%, transparent);
  border-left: 3px solid var(--notice-accent);
  border-radius: var(--r-control);
  background: color-mix(in srgb, var(--notice-accent) 9%, transparent);
  font: var(--t-body-weight) var(--t-ui-size)/var(--t-body-line) var(--font-ui);
}
.notice--warn  { --notice-accent: var(--sky-warn); }
.notice--stop  { --notice-accent: var(--sky-stop); }
.notice--acted { --notice-accent: var(--sky-acted); }
.notice--go    { --notice-accent: var(--sky-go); }
.notice--info  { --notice-accent: var(--sky-cyan); }
.notice__title { font-weight: var(--t-weight-strong); display: block; }

/* ---------------------------------------------------------------------------
 * File rows.
 *
 * Each row is its own HTMX fragment, so a rename or a status change swaps one
 * row and nothing else moves. While a swap is in flight the row keeps its exact
 * height and dims: the list must never reflow under the reader's cursor.
 * ------------------------------------------------------------------------ */
/* The file row.
 *
 * A div grid carrying ARIA table roles rather than a <table>: the same DOM has
 * to reflow into two lines on a phone, and a table forced to display:block
 * loses the semantics that justified it. .filerow is also swapped into place by
 * app.js after an upload, which a <tr> cannot be without its table. */
.filerow {
  display: grid;
  /* Every row is its own grid container, so content-sized tracks would resolve
   * per row: the head's Actions cell is sr-only and folder rows' is empty,
   * which pushed their Status/Size/Modified columns right of the file rows'.
   * All tracks after the name are therefore fixed so every row agrees. */
  grid-template-columns: minmax(0, 1fr) 116px 84px 110px 90px 240px;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-control);
  border: 1px solid transparent;
  transition: background 120ms ease, opacity 120ms ease;
}
.filerow:hover { background: var(--sky-panel-2); }
.filerow + .filerow { border-top: 1px solid var(--sky-edge); border-radius: 0; }

.filerow__ident { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
/* The phone's one-tap type filter (§06), shown only where the filter sheet is
 * otherwise the only way to filter. Reuses .filter-chip so it inherits the
 * control height. */
.filter-quick { display: none; }
.filerow__link { min-width: 0; }
/* Cyan here is correct: a share link IS an action, which is the one thing the
 * colour is spent on. */
.filerow__slug {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  /* The column is 90px and this app's slugs are 16 characters: "/Aeoe_1eTLX81paHE"
   * measures 149.6px and painted straight over the Download button. The
   * proposal's mockup draws a 6-character slug, which is where 90px came from.
   * Truncating keeps the design's slug text and the settled track; the link
   * still opens, and its aria-label still says how many links there are. */
  max-width: 100%;
  min-width: 0;
  color: var(--sky-cyan-text);
  text-decoration: none;
  /* G2 writes 13px; --t-small-size IS 13px, and TestTypeUsesTheScale rejects
   * the literal. Same value, taken from the scale that owns it. */
  font-size: var(--t-small-size);
}
.filerow__slug span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.filerow__slug:hover { text-decoration: underline; }
.filerow__text  { min-width: 0; }
.filerow__name {
  min-width: 0;
  font: 600 14px/1.4 var(--font-ui);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: block;
}
.filerow__meta {
  font: 400 12px/1.45 var(--font-ui);
  color: var(--sky-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.filerow__size, .filerow__date { font-family: var(--font-mono); font-size: var(--t-small-size); color: var(--sky-faint); }
.filerow__status { min-width: 0; }

/* The column headings. Sorting lives here rather than three interactions deep
 * in a modal, so the heading row is the sort control. */
/* The recipient's copy of the list, on a shared folder page. Same rows, same
 * chips, same two content columns — but no status, no share-link column and no
 * selection, so the grid is four tracks rather than six. Actions is narrower
 * than the owner's 240px because there are at most two buttons in it.
 *
 * Defined next to .filerow rather than in a public-page section because it is
 * one override of that grid and nothing else: everything inside a row is the
 * same component the owner sees, which is the whole point of it. */
.filelist--public .filerow {
  grid-template-columns: minmax(0, 1fr) 84px 110px 190px;
}
/* The heading row sits outside the body, so the body's own side padding would
 * shift every data row 32px right of the headings above them — measured before
 * this was written. The owner's list solves it the same way, on
 * .current-folder-items: the rows carry their own padding, so the group around
 * them takes none. */
.filelist--public .panel-sky__body { padding: var(--sp-2) 0; }

.filerow--head {
  border-radius: 0;
  border-bottom: 1px solid var(--sky-edge);
  padding-top: var(--sp-2);
  padding-bottom: var(--sp-2);
  background: var(--sky-panel-2);
  border-top-left-radius: var(--r-panel);
  border-top-right-radius: var(--r-panel);
}
.filerow--head:hover { background: var(--sky-panel-2); }
.filerow--head > span {
  font: var(--t-meta-weight) var(--t-meta-size)/var(--t-meta-line) var(--font-mono);
  letter-spacing: var(--t-track-head);
  text-transform: uppercase;
  color: var(--sky-faint);
}
.sortlink {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  color: inherit;
  text-decoration: none;
  padding: var(--sp-1) var(--sp-2);
  margin: calc(-1 * var(--sp-1)) calc(-1 * var(--sp-2));
  border-radius: 6px;
}
.sortlink:hover { color: var(--sky-bright); background: var(--sky-panel); }
/* The active column reads as emphasis, not as an action: the palette spends
 * cyan on things you are being invited to do. */
.sortlink--active { color: var(--sky-bright); }
.sortlink__dir { font-size: var(--t-meta-size); }

/* Sorting lives on the column headings wherever they fit. The filter sheet
 * keeps its selects only where the headings are hidden, so each width has
 * exactly one sort control rather than two that can disagree. */
@media (min-width: 769px) {
  /* Two class names, because the element also carries .grid-2, which sets
   * display later in this file and would otherwise win on source order. */
  .filter-sort-grid.grid-2 { display: none; }
}

/* Where a search looks. Only rendered inside a folder, because at the root
 * "here and below" and "everywhere" are the same query. */
.scope-toggle {
  display: inline-flex;
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  overflow: hidden;
  background: var(--sky-panel);
  width: max-content;
}
.scope-toggle__option {
  padding: var(--sp-2) var(--sp-3);
  font: var(--t-ui-weight) var(--t-small-size)/var(--t-line-flat) var(--font-ui);
  color: var(--sky-muted);
  text-decoration: none;
}
.scope-toggle__option + .scope-toggle__option { border-left: 1px solid var(--sky-edge); }
.scope-toggle__option:hover { color: var(--sky-bright); background: var(--sky-panel-2); }
.scope-toggle__option--on { background: var(--sky-panel-2); color: var(--sky-bright); }

/* Folders and files were previously distinguishable only by the absence of a
 * status pill, which is not a distinction anyone can see. */
.grouprow { padding: var(--sp-3) var(--sp-3) var(--sp-1); }

/* ---------------------------------------------------------------------------
 * Selection (phase 4).
 *
 * The checkbox sits inside .filerow__ident, ahead of the chip, so it costs the
 * ident's own flex layout nothing beyond its own width — the row grid itself
 * is untouched. Cloaked until Alpine has run: an unchecked box nobody can act
 * on yet is clutter, and the no-JS path has nothing to submit it to (standing
 * constraint 2). Folder rows carry the same control: the bulk delete route
 * takes folder ids alongside file ids, and what the selection bar then offers
 * is the intersection of the two — see fileList.sync in app.js.
 * ------------------------------------------------------------------------ */
.filerow__select {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}
/* Size and accent come from the general checkbox rule now; what this context
 * still needs is the pointer, because the whole row is clickable. */
.filerow__select input { cursor: pointer; }
/* The two master checkboxes — one in the heading row, one in the selection bar.
 * They deliberately do NOT carry .filerow__select. That class is what
 * selectableIDs() means by "a file row's checkbox": giving the masters it too
 * made them count themselves, so select-all pushed two empty ids into the
 * selection and the page reported 22 checkboxes for 20 files.
 *
 * A dedicated class also removes the reason the mobile select-mode rule had to
 * be escaped — neither master is a row cell, so neither is hidden by it. */
.filerow__select--master { display: inline-flex; align-items: center; flex: 0 0 auto; }

/* The bar that replaces the column headings the moment something is checked.
 * Desktop: the same slot fileListHeader sits in, one always display:none
 * while the other shows (x-show="browsing" / x-show="any" on complementary
 * fields of the one fileList component), so nothing above or below either row
 * ever moves. Mobile repositions this to a fixed dock instead — see the 768px
 * block — because fileListHeader itself is display:none there and there is no
 * heading row left to take the place of. */
.selection-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-panel) var(--r-panel) 0 0;
  background: var(--sky-panel-2);
  border-bottom: 1px solid var(--sky-edge);
}
.selection-bar__actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

/* Purely structural: an Alpine scope boundary that must cost the stack-5
 * rhythm above it nothing, since the header, the attention section and
 * FilesListing all still need to be direct flex children of .files-workspace
 * for its gap to apply between them. */
.select-scope { display: contents; }

/* The mobile entry point into selecting. Shown only where the row checkbox is
 * not already sitting on the row unconditionally — see the 768px block, where
 * .filerow__select stays hidden until this is pressed. */
.select-toggle { display: none; }

/* The type chip. Monochrome by construction — see TypeChip in view.go. */
.typechip {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  line-height: var(--t-line-flat);
  background: var(--sky-panel-2);
  border: 1px solid var(--sky-edge);
  color: var(--sky-muted);
}
.typechip svg { width: 13px; height: 13px; }
.typechip em {
  font: 700 7px/1 var(--font-mono);
  font-style: normal;
  letter-spacing: var(--t-track-chip);
}
.typechip--folder { background: transparent; border-style: dashed; }
.typechip--folder svg { width: 15px; height: 15px; }

.filerow--quarantined { border-color: color-mix(in srgb, var(--sky-stop) 45%, transparent); }
.filerow--disabled    { border-color: color-mix(in srgb, var(--sky-acted) 45%, transparent); border-style: dashed; }

/* htmx-request is added by HTMX for the duration of an in-flight swap. */
.filerow.htmx-request { opacity: 0.5; pointer-events: none; }

/* Inline progress inside a row, for an upload in flight. */
.filerow__progress {
  grid-column: 1 / -1;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--sky-edge);
  overflow: hidden;
}
.filerow__progress > i {
  display: block;
  height: 100%;
  background: var(--sky-cyan-fill);
  border-radius: var(--r-pill);
  /* Progress arrives in chunks; linear between them is what makes the bar
   * read as a rate rather than as a series of jumps (§08 — 200ms). */
  transition: width 200ms linear;
}

/* The upload queue borrowed .filerow until the file list became a five-column
 * grid. It is a different thing — one name, one status line, one bar — so it
 * now has its own class instead of inheriting columns it cannot fill. */
.queuerow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3);
  border-radius: var(--r-control);
  border: 1px solid transparent;
}
.queuerow + .queuerow { border-top: 1px solid var(--sky-edge); border-radius: 0; }
.queuerow__name {
  min-width: 0;
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-snug) var(--font-ui);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.queuerow__meta { font: var(--t-body-weight) var(--t-meta-size)/var(--t-line-snug) var(--font-ui); color: var(--sky-faint); }
.queuerow__size { font-family: var(--font-mono); font-size: var(--t-small-size); color: var(--sky-faint); }
.queuerow__progress {
  grid-column: 1 / -1;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--sky-edge);
  overflow: hidden;
}
.queuerow__progress > i {
  display: block;
  height: 100%;
  background: var(--sky-cyan-fill);
  border-radius: var(--r-pill);
  transition: width 200ms linear;
}

/* A list row: one main column that stacks a name over its facts, and a
 * trailing cell for actions. The share panel's existing links, the admin
 * dashboard's recent files and the usage page's largest files are all this
 * shape, and all three wore .filerow — which became the file list's own
 * six-track grid, 640px of fixed columns before the flexible one gets
 * anything. Below about 700px that first track resolves to 0 and the name,
 * URL and facts wrap one character per line while the actions sit on top of
 * them. It is exactly why .queuerow was split out of .filerow, for exactly
 * the same reason. */
.listrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-control);
  border: 1px solid transparent;
}
.listrow + .listrow { border-top: 1px solid var(--sky-edge); border-radius: 0; }
/* min-width:0 so a long filename or URL wraps inside the column rather than
 * widening it past the row. */
.listrow__main { min-width: 0; }

/* A segmented control: one question, one answer, and every answer visible
 * without opening anything. Radios rather than a select because the three
 * choices are the whole vocabulary and each deserves its own hit target; the
 * native input stays in the DOM for the keyboard and the accessibility tree,
 * and is only visually replaced by the label around it. */
/* The recipient's selection bar on a shared folder. Not .selection-bar: that
 * one is the owner's, fixed to the bottom of a workspace with bulk routes
 * behind it. This is a line above a list with one action on it. */
.share-select-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel-2);
}
/* The count and its caveat as one block, so the caveat reads as a note on the
 * action rather than as a sentence loose on the page. 4px because the two are
 * one thought — --sp-2 between them opens a gap the eye reads as separation. */
.share-select-bar__text {
  display: grid;
  gap: 4px;
  min-width: 0;
}
/* The buttons keep their pairing when the bar wraps on a narrow screen, and
 * sit at the end of the line rather than under the count's left edge. */
.share-select-bar__actions {
  flex: 0 0 auto;
  margin-left: auto;
}

.segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--sp-1);
  padding: var(--sp-1);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel-2);
}
.segmented__option {
  display: grid;
  place-items: center;
  min-height: var(--control-h-sm);
  padding: 0 var(--sp-2);
  border-radius: var(--r-control);
  font: var(--t-body-weight) var(--t-small-size)/var(--t-line-snug) var(--font-ui);
  color: var(--sky-muted);
  text-align: center;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
/* The radio itself carries .sr-only: not display:none, which would take it out
 * of the keyboard order and the accessibility tree and leave three unlabelled
 * words. The class is the one the rest of the app already uses for this. */
.segmented__option:hover { color: var(--sky-bright); }
.segmented__option:has(input:checked) {
  background: var(--sky-panel);
  color: var(--sky-bright);
  box-shadow: inset 0 0 0 1px var(--sky-edge);
}
.segmented__option:has(input:focus-visible) { outline: 2px solid var(--sky-cyan); outline-offset: 2px; }
@media (max-width: 560px) {
  /* Three labels across a phone reads as three cropped words. */
  .segmented { grid-auto-flow: row; }
}

/* ---------------------------------------------------------------------------
 * App shell
 * ------------------------------------------------------------------------ */
.shell { display: grid; grid-template-columns: 264px 1fr; min-height: 100vh; }

/* On a desktop the shell is exactly the viewport and neither column grows it:
 * the sidebar is furniture that never scrolls away — its bottom cluster stays
 * on screen however long the list runs — and the workspace column is the only
 * thing that scrolls. minmax(0, 1fr) keeps a long page from stretching the
 * row (and with it the sidebar) back to content height. Mobile keeps the plain
 * document scroll: the sidebar is hidden there and the tab bar is fixed. */
@media (min-width: 901px) {
  .shell { height: 100vh; grid-template-rows: minmax(0, 1fr); }
  .shell__side,
  .shell__main { overflow-y: auto; }

  /* Bulma sets html { overflow-y: scroll } to keep page-length changes from
   * shifting the layout sideways. On a shell that is exactly the viewport and
   * scrolls in its own columns, that scrollbar can never scroll anything — it
   * is painted beside the workspace's real one and reads as two scrollbars.
   * Scoped to the shell layout, and to the widths where the shell owns the
   * viewport: mobile keeps the plain document scroll it is built on. */
  html.app-shell { overflow-y: hidden; }
}

.shell__side {
  border-right: 1px solid var(--sky-edge);
  padding: var(--sp-5) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  background: var(--sky-panel);
}
/* Flex column so the legal footer's margin-top:auto reaches the bottom of the
 * shell. Without it the footer stops directly under a short page's content and
 * reads as loose text rather than as a footer. */
.shell__main {
  /* position:relative so this is the containing block for its own absolutely
   * positioned descendants. Without it they resolve against the initial
   * containing block instead, which means overflow:auto here does not clip
   * them and they extend the *document* — every .sr-only label in a file list
   * is one of those, so a long list gave the page a second scrollbar of its
   * own beside this column's. It measured 237px on a 45-row list: the last
   * label sat at y=1136 in a 900px viewport. */
  position: relative;
  padding: var(--sp-5);
  padding-bottom: 0;
  /* max-width: 1180px; */
  width: 100%;
  display: flex;
  flex-direction: column;
}
/* The app footer pins to the bottom with margin-top:auto, which resolves to
 * zero once the content fills the viewport — its border then sits directly on
 * the last panel. Main carries the minimum gap itself: flex margins never
 * collapse, so this holds on full pages while auto still absorbs the slack on
 * short ones. */
.shell__main > main { margin-bottom: var(--sp-6); }
.shell__mobile-tools { display: none; }

.navlink {
  /* flex, not block. Icons phase 2 guards this: the rule already said
   * display: block, and the plan says do not overwrite a display without
   * saying so. The proposal settles it — .mk-nav a is drawn
   * display:flex; align-items:center; gap:9px (ui-redesign-proposals.html,
   * ".mk-nav a"). Block-level flex still fills the rail, so the click target
   * is the width it was. The gap is --sp-2 rather than the drawn 9px: 8px is
   * on the spacing scale and 9px is not. */
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 8px 12px;
  border-radius: var(--r-control);
  color: var(--sky-muted);
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-snug) var(--font-ui);
  text-decoration: none;
}
.navlink:hover { background: var(--sky-panel-2); color: var(--sky-bright); }
.navlink[aria-current="page"] { background: var(--sky-panel-2); color: var(--sky-bright); }

.brand { display: inline-flex; align-items: center; gap: var(--sp-2); text-decoration: none; }
.brand__dot {
  width: 10px; height: 10px; border-radius: 3px;
  background: var(--sky-cyan);
  box-shadow: 0 0 14px color-mix(in srgb, var(--sky-cyan) 55%, transparent);
}
.brand__name {
  font: var(--t-label-weight) var(--t-meta-size)/var(--t-meta-line) var(--font-mono);
  letter-spacing: var(--t-track-eyebrow);
  text-transform: uppercase;
  color: var(--sky-cyan-text);
}

.theme-toggle__icon {
  width: 1.2em;
  font-size: var(--t-body-size);
  line-height: var(--t-line-flat);
  text-align: center;
}

/* The mobile tab bar from screens 4c and 8d. Hidden on desktop, where the
 * sidebar does the same job. */
.tabbar { display: none; }

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .shell__side { display: none; }
  /* Clears the tab bar the same way the selection bar does, from the same
   * token, and adds the home indicator the flat 84px never did. */
  .shell__main { padding: var(--sp-4) var(--sp-3) calc(var(--tabbar-h) + env(safe-area-inset-bottom) + var(--sp-4)); }
  .shell__main > main { margin-bottom: var(--sp-4); }
  .shell__mobile-tools {
    display: flex;
    justify-content: flex-end;
    margin-bottom: var(--sp-3);
  }
  .tabbar {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    position: fixed;
    inset: auto 0 0 0;
    background: var(--sky-panel);
    border-top: 1px solid var(--sky-edge);
    padding: 8px 4px calc(8px + env(safe-area-inset-bottom));
    z-index: 40;
  }
  .tabbar a,
  .tabbar button {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    text-align: center;
    min-height: 44px;
    padding: 6px 2px;
    /* --t-meta-line, not --t-line-snug. The meta step carries its own
     * line-height of 1 and this rule was reaching the size while inventing a
     * 1.4 — the exact failure the type scale's comment describes. It went
     * unnoticed while the label was the only thing in the box; stacking an
     * icon on top of it made the 4.4px of invented leading push the bar from
     * its measured 61px to 62.70. The scale's own pairing puts it back. */
    font: var(--t-meta-weight) var(--t-meta-size)/var(--t-meta-line) var(--font-ui);
    color: var(--sky-muted);
    text-decoration: none;
  }
  /* The bar's label is --t-meta-size (11px), and .icon is 1em, so an icon here
   * would come out at 11px — smaller than anything the proposal draws (its
   * floor is the 14px .mrow__k glyph; the sidebar draws 15px against 13px
   * text, about 1.15em). 1.3em puts these at 14.3px, on that floor. This is
   * the one place an icon does not simply track its control's type size, and
   * it is sized here rather than on .icon so no other surface moves. */
  .tabbar .icon { width: 1.3em; height: 1.3em; }
  .tabbar a[aria-current="page"] { color: var(--sky-cyan-text); }
  .tabbar button {
    align-self: center;
    /* margin-top: -18px; */
    border: 1px solid var(--sky-cyan);
    border-radius: var(--r-pill);
    background: var(--sky-cyan);
    color: var(--sky-action-ink);
    cursor: pointer;
    box-shadow: var(--shadow-lift);
  }
}

/* Public pages (landing, pricing, share, auth) have no sidebar. */
.page { max-width: 1080px; margin: 0 auto; padding: var(--sp-5) var(--sp-4) var(--sp-6); }
.page--narrow { max-width: 460px; }

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-4);
  border-bottom: 1px solid var(--sky-edge);
}
.topbar__links { display: flex; align-items: center; gap: var(--sp-4); }
/* :not(.btn) matters: without it this rule outranks .btn--primary on
 * specificity and paints the "Create an account" button's label muted cyan on
 * a cyan pill, which is invisible. Nav links are styled here; buttons keep
 * their own colours wherever they appear. */
.topbar__links a:not(.btn) { color: var(--sky-muted); text-decoration: none; font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-flat) var(--font-ui); }
.topbar__links a:not(.btn):hover { color: var(--sky-bright); }

.footer-sky {
  border-top: 1px solid var(--sky-edge);
  margin-top: var(--sp-6);
  padding: var(--sp-5) var(--sp-4);
  color: var(--sky-faint);
  font-size: var(--t-small-size);
}
.footer-sky a { color: var(--sky-muted); text-decoration: none; }
.footer-sky a:hover { color: var(--sky-bright); }
.footer-sky--app { margin-top: auto; padding-inline: 0; }
/* The seller identity is a legal requirement, not a design element: quiet, but
   never hidden and never smaller than the rest of the footer. */
.footer-sky__seller { margin-top: var(--sp-3); max-width: 80ch; }

.legal-page { max-width: 780px; }
.legal-head { padding-bottom: var(--sp-5); border-bottom: 1px solid var(--sky-edge); }
.legal-meta {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
  margin: 0;
}
.legal-meta > div {
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel);
}
.legal-meta dt { font: var(--t-label-weight) var(--t-label-size)/var(--t-label-line) var(--font-mono); letter-spacing: var(--t-track-eyebrow); text-transform: uppercase; color: var(--sky-faint); }
/* The value under a 10px label had no size of its own and fell back to the
 * 16px body, so a version string outweighed the word naming it. */
.legal-meta dd { margin: var(--sp-2) 0 0; overflow-wrap: anywhere; font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui); }
.legal-prose { max-width: 72ch; }
.legal-prose h2 { margin: var(--sp-6) 0 var(--sp-3); font: var(--t-h2-weight) var(--t-h2-size)/var(--t-h2-line) var(--font-ui); letter-spacing: var(--t-h2-track); }
.legal-prose h3 { margin: var(--sp-5) 0 var(--sp-3); font: var(--t-ui-weight) var(--t-body-size)/var(--t-line-snug) var(--font-ui); }
.legal-prose p { margin: 0 0 var(--sp-4); }
.legal-prose ul,
.legal-prose ol { margin: 0 0 var(--sp-4); padding-left: 1.5rem; }
.legal-prose li + li { margin-top: var(--sp-2); }
.legal-prose a { color: var(--sky-cyan-text); text-underline-offset: 0.18em; }
.legal-related { display: flex; flex-direction: column; gap: var(--sp-3); padding-top: var(--sp-5); border-top: 1px solid var(--sky-edge); }
.check-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel);
  cursor: pointer;
  /* The sentence beside the box is a consent line, not body copy. Without
   * this it inherited 16px from body and outweighed the thing it qualifies. */
  font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui);
}
/* No top offset. The box aligns to the start of its grid cell, and the
 * question is whether that lands on the first line's optical centre: the
 * offset needed is (line box - box) / 2. Against the 13px/1.5 text this row
 * now carries, that is (19.5 - 18) / 2 = 0.75px — under a pixel, so nothing
 * is better than a number. It mattered when this text was body-sized: a
 * 16px/1.6 line box is 25.6px and wanted 3.8px, which is what the 3px here
 * used to be. If the row's type changes again, re-derive it rather than
 * nudging it by eye. */
/* The box is 24px for the pointer target and the line beside it is 19.5px, so
 * the box is now taller than the text it labels and hangs below the first
 * line's centre by (19.5 - 24) / 2.
 *
 * Written as that arithmetic rather than as -2.25px, because the last number
 * that sat here was calibrated for text this row no longer has and nobody
 * noticed for two phases. This one re-derives itself if either the type step or
 * the box size moves. */
.check-row input { margin-block-start: calc((var(--t-small-line) * var(--t-small-size) - 24px) / 2); }
.check-row a { color: var(--sky-cyan-text); }
.checkout-disclosure { padding: var(--sp-3); border-left: 3px solid var(--sky-cyan); background: color-mix(in srgb, var(--sky-cyan) 7%, transparent); }
.checkout-disclosure a { color: var(--sky-cyan-text); }

/* ---------------------------------------------------------------------------
 * Layout helpers.
 *
 * Vertical rhythm is owned by the containing stack, never by its children:
 * 8px for tightly related controls, 12px inside a component, 20px for normal
 * groups, and 32px between page sections. This keeps grids, panels, and
 * fragments on the same rhythm.
 * ------------------------------------------------------------------------ */
.stack-2 { display: flex; flex-direction: column; gap: var(--sp-2); }
.stack-3 { display: flex; flex-direction: column; gap: var(--sp-3); }
.stack,
.stack-4 { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack-5 { display: flex; flex-direction: column; gap: var(--sp-5); }
.row     { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.row--between { justify-content: space-between; }
/* A field and the control that acts on it, as one row. Both children take
 * --control-h from their own rules, so this only owns the gap and the
 * stretch — which is what retires the `height: 100%` patch that used to make
 * the search field match the button beside it. */
.control-group { display: flex; align-items: stretch; gap: var(--sp-2); }
.control-group > :first-child { flex: 1; min-width: 0; }
.grid-2  { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); }
.grid-3  { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); }
.muted   { color: var(--sky-muted); }
.cyan    { color: var(--sky-cyan-text); }
/* A link inside muted prose cannot be told apart by colour alone: cyan on
 * --sky-muted is 1.41:1, and 3:1 is the floor for colour as the only cue.
 * Standing rule 3 still holds — cyan means action and a link is an action —
 * this only stops colour being the ONLY signal. Scoped to the muted pair, so
 * breadcrumbs and table links, which are not surrounded by text, keep their
 * clean look. */
.muted a,
.check-row a,
.checkout-disclosure a { text-decoration: underline; text-underline-offset: 2px; }
.stop    { color: var(--sky-stop); }
.go      { color: var(--sky-go); }
.warn    { color: var(--sky-warn); }
.acted   { color: var(--sky-acted); }
.grow    { flex: 1; min-width: 0; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 900px) {
  .grid-2, .grid-3 { grid-template-columns: 1fr; }
}

@media (max-width: 600px) {
  .legal-meta { grid-template-columns: 1fr; }
  .footer-sky > .row { align-items: flex-start; }
}

/* Screen-reader-only text, for the labels a control needs and a sighted reader
 * gets from context. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Skip link — the first focusable thing on every page. */
.skip-link {
  position: absolute; left: -9999px;
  background: var(--sky-cyan); color: var(--sky-action-ink);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-control); z-index: 100;
}
.skip-link:focus { left: var(--sp-3); top: var(--sp-3); }

/* ---------------------------------------------------------------------------
 * Alpine
 *
 * x-cloak marks an element Alpine has not initialised yet. Without this rule
 * every menu and sheet renders open for the frame before Alpine runs, which
 * reads as a flicker on the first paint of every page — the one visible bug
 * this stack reliably produces. It has to be plain CSS, not a class Alpine
 * adds, precisely because Alpine has not run yet.
 * ------------------------------------------------------------------------ */
[x-cloak] { display: none !important; }

/* ---------------------------------------------------------------------------
 * HTMX
 * ------------------------------------------------------------------------ */
.htmx-indicator { opacity: 0; transition: opacity 160ms ease; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; }

/* A swapping region keeps its height and dims rather than collapsing, so the
 * page below it does not jump while a fragment is in flight. Carried by the
 * file row (the poll swaps it whole every 3s) and by .files-listing (the
 * mutations swap it whole); it only ever engages while htmx has put
 * .htmx-swapping on the element. */
.swap-stable.htmx-swapping { opacity: 0.4; transition: opacity 100ms ease; }

/* ---------------------------------------------------------------------------
 * Motion — §08.
 *
 * Every state the server can produce gets a transition, each with one owner:
 * htmx animates what came from the server (its own classes below), Alpine
 * animates what never left the browser (the .mo-* class lists its x-transition
 * directives apply), plain CSS animates what only the top layer can do
 * (native <dialog> entrances). Never both systems on one element — guarded by
 * TestNoElementHasBothHXTransitionAndXTransition.
 *
 * The row-enter class is applied by app.js at insertion time and by nothing
 * else, which is what keeps a completed upload animated and first paint still.
 * ------------------------------------------------------------------------ */

@keyframes mo-row-enter {
  from { opacity: 0; transform: translateY(8px) scale(0.99); }
}
.filerow.is-entering { animation: mo-row-enter 320ms var(--sky-ease); }

/* The moment a file becomes usable: the settled pill rises in with the one
 * deliberate overshoot in the design. Two triggers, one animation: htmx's
 * settle window (.htmx-added, when a poll tick delivers the answer) and
 * app.js's own comparison (is-settled, when the upload queue's plain-fetch
 * refresh beats the poll — that path carries no htmx classes to key off).
 *
 * The animation lands on the status cell rather than on .pill itself — the
 * cell blockifies as a grid item and wraps exactly the pill, and keeping the
 * word ".pill" out of this selector keeps it from shadowing the mobile rules
 * a first-match stylesheet guard reads. */
@keyframes mo-pill-in {
  from { opacity: 0; transform: translateY(5px) scale(0.95); }
  65%  { opacity: 1; transform: translateY(-1px) scale(1.05); }
}
.filerow.htmx-added .filerow__status,
.filerow.is-settled .filerow__status {
  animation: mo-pill-in 260ms var(--sky-ease) 130ms both;
}

/* The exit half of the settle (§08: "out: fade + translateY −4px, 220ms").
 * app.js applies is-departing only when the incoming status actually differs
 * from the outgoing one, so a poll tick that changed nothing does not animate
 * — the list must not shimmer every three seconds. */
@keyframes mo-pill-out {
  to { opacity: 0; transform: translateY(-4px); }
}
.filerow.is-departing .filerow__status {
  animation: mo-pill-out 220ms var(--sky-ease) both;
}

/* A deleted row leaves before the listing lands (§08: "Row leave · 160ms ·
 * ease-in"). Applied by app.js at htmx:beforeRequest, never by a template. */
@keyframes mo-row-leave {
  to { opacity: 0; transform: translateY(-4px) scale(0.99); }
}
.filerow.is-leaving {
  animation: mo-row-leave 160ms ease-in both;
}

/* Dialogs are native <dialog> elements (phase 3), so their motion is CSS on
 * [open] — Alpine never touches their visibility, and x-transition could not
 * reach into the top layer anyway. Entrance only: dialog.close() leaves
 * immediately, and nothing in this product waits on an exit animation. */
@keyframes mo-dialog-in {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
}
.sheet[open],
.confirm-dialog[open] { animation: mo-dialog-in 160ms var(--sky-ease); }

@keyframes mo-sheet-up {
  from { opacity: 0; transform: translateY(100%); }
}
.upload-sheet[open] { animation: mo-sheet-up 260ms var(--sky-ease-sheet); }

@keyframes mo-scrim-in { from { opacity: 0; } }
.sheet[open]::backdrop,
.confirm-dialog[open]::backdrop { animation: mo-scrim-in 200ms ease-out; }

/* Sort and filter swaps run through View Transitions (transition:true on the
 * listing target). The default cross-fade is already opacity-only; this just
 * holds it to the report's 220ms instead of the UA default. Browsers without
 * the API skip straight to the swap. */
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 220ms; }

/* Class lists for the two things Alpine shows and hides: the selection bar
 * (first row checked / last unchecked — faster leaving than arriving,
 * because dismissal should feel instant and arrival noticed) and the row menu
 * panel (scaled from the corner that summoned it; anything slower than ~150ms
 * reads as lag). The CSP build cannot evaluate expressions, so these are
 * literal class names, resolved by sky.css rather than by Alpine. */
.mo-rise-in   { transition: opacity 200ms var(--sky-ease), transform 200ms var(--sky-ease); }
.mo-from-below { opacity: 0; transform: translateY(14px); }
.mo-rest       { opacity: 1; transform: none; }
.mo-fade-out   { transition: opacity 140ms ease-out; }
.mo-faded      { opacity: 0; }

.mo-pop-in    { transition: opacity 130ms var(--sky-ease), transform 130ms var(--sky-ease); transform-origin: top right; }
.mo-pop-start { opacity: 0; transform: translateY(-4px) scale(0.94); }
.mo-drop-out  { transition: opacity 110ms ease-in, transform 110ms ease-in; transform-origin: top right; }
.mo-dropped   { opacity: 0; transform: translateY(-2px) scale(0.97); }

/* Reduced motion is a branch through this section, not an off-switch over it.
 * Transforms go — travel is what vestibular disorders punish — and every fade
 * that carried information survives at 80ms: a user who asked for less motion
 * still has to see that the scan finished. The meter and progress fills keep
 * their width transitions deliberately; growth along a track is information,
 * not decoration. Replaces the blanket duration-kill this file used to ship. */
@media (prefers-reduced-motion: reduce) {
  @keyframes mo-fade { from { opacity: 0; } }

  .filerow.is-entering {
    animation-name: mo-fade;
    animation-duration: 80ms;
    animation-timing-function: ease-out;
  }
  .filerow.htmx-added .filerow__status,
  .filerow.is-settled .filerow__status {
    animation-name: mo-fade;
    animation-duration: 80ms;
    animation-delay: 0ms;
    animation-timing-function: ease-out;
  }
  .sheet[open],
  .confirm-dialog[open],
  .upload-sheet[open] {
    animation-name: mo-fade;
    animation-duration: 80ms;
    animation-timing-function: ease-out;
  }
  /* Holds go to zero, not to 80ms: a hold is latency, and the reader who
   * asked for less motion must not be made to wait for it. The fades below
   * still carry the information. */
  .filerow.is-departing .filerow__status,
  .filerow.is-leaving {
    animation-name: mo-fade;
    animation-duration: 80ms;
    animation-direction: reverse;
  }
  .sheet[open],
  .compact-sheet[open],
  .share-sheet[open] {
    animation-name: mo-fade;
    animation-duration: 80ms;
  }

  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 80ms !important; }

  /* Alpine's entrances keep their fades and drop their travel. */
  .mo-from-below, .mo-pop-start { transform: none; }
  .mo-rise-in, .mo-fade-out, .mo-pop-in, .mo-drop-out { transition-duration: 80ms; }

  /* Scoped to .action-menu on purpose, and it is specificity doing the work,
   * not source order. The 768px block redefines these same four classes into a
   * 260ms sheet rise, and it sits ~800 lines LATER in this file — a media query
   * adds no specificity, so the single-class rules above would lose to it and a
   * phone with reduced motion would keep the travel. Two classes beat one
   * wherever they appear. Kept in this block because sky.css carries exactly
   * one reduced-motion branch, which TestReducedMotionBlockExists enforces. */
  .action-menu .mo-pop-in,
  .action-menu .mo-drop-out { transition-duration: 80ms; }
  .action-menu .mo-pop-start,
  .action-menu .mo-dropped { transform: none; }

  /* The inspector drawer slides in from the right edge; reduced motion keeps
   * only the fade. */
  .inspector-panel.htmx-added {
    animation-name: mo-fade;
    animation-duration: 80ms;
    animation-timing-function: ease-out;
  }
}

/* ---------------------------------------------------------------------------
 * Inspector rail (§07).
 *
 * A row click swaps /files/{id}/panel — the detail page's own record blocks —
 * into this dock, which sits outside #files-listing so a mutation that swaps
 * the listing never takes the panel with it. Overlay rather than column: it
 * covers the list's right edge instead of reflowing the grid under the
 * reader's cursor. It does not exist on a phone; there the name link is just
 * a link.
 * ------------------------------------------------------------------------ */
.inspector {
  position: fixed;
  top: var(--sp-4);
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 35;
  width: min(420px, calc(100vw - 2 * var(--sp-4)));
  /* The dock is a coordinate, not a surface: while it is empty it must not
   * stand in front of the row actions underneath it. Only the panel itself
   * takes pointers, once it exists. */
  pointer-events: none;
}

.inspector-panel {
  pointer-events: auto;
  height: 100%;
  overflow-y: auto;
  background: var(--sky-panel);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-lift);
}
.inspector-panel__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
  background: var(--sky-panel);
  border-bottom: 1px solid var(--sky-edge);
}
.inspector-panel__name {
  min-width: 0;
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-snug) var(--font-ui);
  color: var(--sky-bright);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.inspector-panel__name:hover { color: var(--sky-cyan-text); }
/* Two actions on one row, equal width (§04 draws Download and Share side by
 * side under the medallion). flex: 1 rather than width: 100% because there are
 * two of them now; a lone Download still fills the row. */
.inspector-panel__actions {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border-bottom: 1px solid var(--sky-edge);
}
.inspector-panel__actions .btn { flex: 1; }
.inspector-panel__body { padding: var(--sp-3); }

/* The preview is the detail page's stage at rail width: same card, same
 * caption's job, clipped corners where the stage meets the card. */
.inspector-panel__preview { overflow: hidden; }
.inspector-panel__preview .record__head { cursor: default; }
.inspector-panel__preview .viewer { border-radius: 0; }

/* The records are the detail page's component at a third of the width, so a
 * few of its layouts get narrow-context overrides here. The share rows are
 * the same component the /shares list uses in a five-column grid; at this
 * width they become stacked cards instead. */
.inspector-panel .file-detail__meta { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.inspector-panel .record__body .filerow {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
}
.inspector-panel .record__body .filerow + .filerow { margin-top: var(--sp-2); border-top: 1px solid var(--sky-edge); }
.inspector-panel .record__body .filerow .row { flex-wrap: wrap; }
/* Permission checkboxes: the shared .row wraps between the box and its label
 * at this width, stranding the checkbox on its own line. */
.inspector-panel #share-panel label.row { flex-wrap: nowrap; align-items: flex-start; }
.inspector-panel #share-panel label.row span { flex: 1; min-width: 0; }
.inspector-panel #share-panel .sheet__sticky-action { margin: 0; padding: 0; border: 0; background: none; }

/* The medallion is the type chip at 56px — glyph and extension, never bytes.
 * Loading a file to fill a small square is not a preview, it is a bill. */
.medallion {
  flex: 0 0 auto;
  width: 56px;
  height: 56px;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  line-height: var(--t-line-flat);
  background: var(--sky-panel-2);
  border: 1px solid var(--sky-edge);
  color: var(--sky-muted);
}
.medallion svg { width: 20px; height: 20px; }
.medallion em {
  font: 700 9px/1 var(--font-mono);
  font-style: normal;
  letter-spacing: var(--t-track-chip);
}

/* The panel arrives from the server (an htmx innerHTML swap), so htmx owns
 * its entrance: a short slide from the right edge while .htmx-added is
 * present during settle. */
@keyframes mo-drawer-in {
  from { opacity: 0; transform: translateX(12px); }
}
.inspector-panel.htmx-added { animation: mo-drawer-in 180ms var(--sky-ease); }

@media (max-width: 768px) {
  .inspector { display: none; }
}

/* ---------------------------------------------------------------------------
 * Tables — used by billing history, usage, and the admin screens.
 * ------------------------------------------------------------------------ */
.table-sky { width: 100%; border-collapse: collapse; }
.table-sky th {
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  font: var(--t-label-weight) var(--t-label-size)/var(--t-label-line) var(--font-mono);
  letter-spacing: var(--t-label-track);
  text-transform: uppercase;
  color: var(--sky-faint);
  border-bottom: 1px solid var(--sky-edge);
  white-space: nowrap;
}
.table-sky td {
  padding: var(--sp-3);
  border-bottom: 1px solid var(--sky-edge);
  font-size: var(--t-ui-size);
  vertical-align: top;
}
.table-sky td.num, .table-sky th.num { text-align: right; }
#pricing-table.table-sky td { padding: var(--sp-3) 0; }
.table-wrap { 
  overflow-x: auto;
  max-height: 20rem;
}

/* ---------------------------------------------------------------------------
 * Upload target — the one place with a drop shadow, per the design.
 * ------------------------------------------------------------------------ */
.dropzone {
  border: 2px dashed var(--sky-edge);
  border-radius: var(--r-panel);
  background: var(--sky-panel);
  box-shadow: var(--shadow-lift);
  padding: var(--sp-6) var(--sp-5);
  text-align: center;
  transition: border-color 140ms ease, background 140ms ease;
}
.dropzone.is-over {
  border-color: var(--sky-cyan);
  background: color-mix(in srgb, var(--sky-cyan) 8%, var(--sky-panel));
}

/* ---------------------------------------------------------------------------
 * Communities landing page.
 * ------------------------------------------------------------------------ */
.community-hero,
.community-trust {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, 0.86fr);
  gap: var(--sp-6);
  align-items: center;
}

.community-artifact {
  position: relative;
  min-height: 360px;
  border-radius: 28px;
  border: 1px solid color-mix(in srgb, var(--sky-cyan) 24%, var(--sky-edge));
  background:
    radial-gradient(circle at 85% 15%, color-mix(in srgb, var(--sky-cyan) 24%, transparent), transparent 28%),
    radial-gradient(circle at 12% 85%, color-mix(in srgb, var(--sky-acted) 26%, transparent), transparent 32%),
    linear-gradient(145deg, var(--sky-panel), var(--sky-panel-2));
  box-shadow: var(--shadow-lift);
  overflow: hidden;
  padding: var(--sp-5);
}

.community-artifact::before {
  content: "";
  position: absolute;
  inset: 42px 30px auto auto;
  width: 160px;
  height: 160px;
  border: 1px dashed color-mix(in srgb, var(--sky-cyan) 45%, transparent);
  border-radius: 50%;
  opacity: 0.65;
}

.community-post,
.community-link-card,
.community-file-stack {
  position: relative;
  border: 1px solid var(--sky-edge-2);
  border-radius: var(--r-panel);
  background: color-mix(in srgb, var(--sky-panel) 86%, transparent);
  backdrop-filter: blur(10px);
}

.community-post {
  max-width: 310px;
  padding: var(--sp-4);
}

.community-link-card {
  margin: var(--sp-5) 0 0 auto;
  max-width: 360px;
  padding: var(--sp-4);
  display: grid;
  gap: var(--sp-3);
}

.community-file-stack {
  margin-top: var(--sp-4);
  max-width: 300px;
  padding: var(--sp-2);
  display: grid;
  gap: var(--sp-2);
}

.community-file-stack span {
  display: block;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-control);
  background: var(--sky-panel-2);
  color: var(--sky-muted);
  font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-mono);
}

.community-scenarios,
.community-fit-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4);
}

.community-scenario,
.community-fit,
.community-flow,
.community-checklist {
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
  background: var(--sky-panel);
}

.community-scenario,
.community-fit {
  padding: var(--sp-4);
}

.community-scenario:nth-child(2),
.community-fit:nth-child(2) {
  background: color-mix(in srgb, var(--sky-cyan) 10%, var(--sky-panel));
  border-color: color-mix(in srgb, var(--sky-cyan) 32%, var(--sky-edge));
}

.community-flow {
  padding: var(--sp-5);
}

.community-steps {
  display: grid;
  gap: var(--sp-3);
}

.community-step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-3);
  padding: var(--sp-3);
  border-radius: var(--r-control);
  background: var(--sky-panel-2);
}

.community-step .num {
  grid-row: span 2;
  color: var(--sky-cyan-text);
  font-weight: var(--t-weight-strong);
}

.community-step p { margin: 0; }

.community-trust {
  padding: var(--sp-5);
  border: 1px solid color-mix(in srgb, var(--sky-go) 24%, var(--sky-edge));
  border-radius: 28px;
  background: linear-gradient(135deg, color-mix(in srgb, var(--sky-go) 8%, var(--sky-panel)), var(--sky-panel));
}

.community-checklist {
  padding: var(--sp-4);
  display: grid;
  gap: var(--sp-2);
}

.community-checklist span {
  display: flex;
  gap: var(--sp-2);
  align-items: baseline;
  color: var(--sky-bright);
  font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-ui);
}

.community-checklist span::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--sky-go);
  flex: 0 0 auto;
  transform: translateY(-1px);
}

@media (max-width: 900px) {
  .community-hero,
  .community-trust,
  .community-scenarios,
  .community-fit-grid {
    grid-template-columns: 1fr;
  }
  .community-artifact { min-height: auto; }
  .community-link-card { margin-left: 0; }
}

/* ---------------------------------------------------------------------------
 * Moderator trust page.
 * ------------------------------------------------------------------------ */
.moderator-hero,
.moderator-routes,
.moderator-policy {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 0.72fr);
  gap: var(--sp-6);
  align-items: start;
}

.moderator-summary,
.moderator-routes,
.moderator-policy,
.moderator-check,
.moderator-route {
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
  background: var(--sky-panel);
}

.moderator-summary {
  padding: var(--sp-5);
  background:
    radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--sky-go) 20%, transparent), transparent 34%),
    var(--sky-panel);
  box-shadow: var(--shadow-lift);
}

.moderator-summary strong {
  display: block;
  margin: var(--sp-3) 0;
  font: var(--t-h2-weight) var(--t-h2-size)/var(--t-h2-line) var(--font-ui);
}

.moderator-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4);
}

.moderator-check,
.moderator-route {
  padding: var(--sp-4);
}

.moderator-route-list {
  display: grid;
  gap: var(--sp-3);
}

.moderator-routes,
.moderator-policy {
  padding: var(--sp-5);
}

.moderator-policy {
  border-color: color-mix(in srgb, var(--sky-warn) 24%, var(--sky-edge));
  background: linear-gradient(135deg, color-mix(in srgb, var(--sky-warn) 7%, var(--sky-panel)), var(--sky-panel));
}

@media (max-width: 900px) {
  .moderator-hero,
  .moderator-routes,
  .moderator-policy,
  .moderator-grid {
    grid-template-columns: 1fr;
  }
}

/* Empty states get the same panel treatment but no shadow — they are a place
 * where nothing is happening, not an invitation to drop. */
.empty {
  padding: var(--sp-6) var(--sp-4);
  text-align: center;
  color: var(--sky-muted);
}
.empty__title { font: var(--t-h2-weight) var(--t-h2-size)/var(--t-h2-line) var(--font-ui); color: var(--sky-bright); margin-bottom: var(--sp-2); }

/* The media stage is a contained record, not a viewport-sized void: the page
 * opens with what the file is and what you can do with it, and the preview
 * waits below in its own card, captioned to say nothing was generated to make
 * it. A 16/9 stage holds image and video; a PDF gets a taller paper sheet; an
 * audio file gets a player without any black box at all. */
.viewer {
  display: grid;
  /* minmax(0, 1fr) rather than the implicit auto row, and it is the whole
   * reason a tall image fits. A grid item's max-height:100% resolves against
   * its grid area, an auto row is sized by its content, and a percentage
   * against a content-sized track is indefinite — so max-height was being
   * ignored, the row grew past the 16:9 box, and overflow:hidden cropped the
   * bottom off every portrait image. A 1fr row with a zero floor is definite
   * and can shrink, which is what the percentage needed. */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  aspect-ratio: 16 / 9;
  background: #050709;
  padding: var(--sp-3);
  overflow: hidden;
  border-radius: var(--r-panel);
}
.viewer--paper { aspect-ratio: 4 / 3; }
.viewer--audio {
  aspect-ratio: auto;
  background: none;
  padding: var(--sp-3);
}
.viewer__media {
  display: block;
  max-width: 100%;
  max-height: 100%;
  /* Grid items floor at min-content by default, which lets a tall image refuse
   * to shrink and push out of the box the row above just made definite. */
  min-width: 0;
  min-height: 0;
  /* The preview shows the file, all of it. object-fit only bites if something
   * later gives this element a size of its own, and then contain is the answer
   * that keeps the whole frame rather than cropping to fill. */
  object-fit: contain;
  margin: auto;
  border-radius: var(--r-control);
}
.viewer__audio { width: min(560px, 100%); margin: auto; }
.viewer__frame { width: 100%; height: 100%; border: 0; background: #fff; border-radius: var(--r-control); }

/* Make the custom elements actually fill the 16:9 viewer */
.viewer > video-player,
.viewer > video-player > video-skin {
	width: 100%;
	height: 100%;
	min-width: 0;
	min-height: 0;
}
video-player > video-skin {
  --media-border-radius: none;
}
video-player > video-skin > img {
  display: none !important;
}

/* ---------------------------------------------------------------------------
 * Record blocks (§07) — Details, Safety, Sharing, and later History and
 * Delivery. One component on the detail page, later re-rendered by the
 * phase-7 inspector rail. Native <details>, open by default; app.js folds
 * them on phones so the page has a bottom there.
 * ------------------------------------------------------------------------ */
.record {
  background: var(--sky-panel);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
}
.record__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4);
  cursor: pointer;
  list-style: none;
}
.record__head::-webkit-details-marker { display: none; }
.record > summary .t-h2 { flex: 0 0 auto; }
.record[open] > .record__head,
.record__head:hover { border-bottom: 1px solid var(--sky-edge); }
.record__body { padding: var(--sp-4); }

/* History block (phase 8): a plain trail of what happened, newest first. Each
 * line is the sentence on the left and its time on the right — the time stays
 * quiet (muted, tabular) so the eye reads the events, not the clock. */
.history-list { list-style: none; margin: 0; padding: 0; }
.history-item {
  display: grid;
  grid-template-columns: 6px minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
/* The dot is the event's status meaning, never its type — the same rule the
 * pill obeys, which is what makes colour here legitimate. currentColor means
 * each tone colours its own dot with no per-tone dot rules. */
.history-item__dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; align-self: center;
}
.history-item__dot--go    { color: var(--sky-go); }
.history-item__dot--warn  { color: var(--sky-warn); }
.history-item__dot--stop  { color: var(--sky-stop); }
.history-item__dot--acted { color: var(--sky-acted); }
.history-item__dot--quiet { color: var(--sky-muted); }
/* --sp-1, not the 2px G4 writes: 2px is off the 4/8/12/20/32/52 scale and
 * TestSpacingUsesTheScale rejects it. Unlike .grouprow's 2px, which holds a
 * measured row count, nothing measured depends on this gap — so it takes the
 * scale rather than an exception entry. */
.history-item__text { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.history-item + .history-item { border-top: 1px solid var(--sky-edge); }
.history-item__when { white-space: nowrap; flex: none; }

.file-detail__top {
  /* Sticky name and status on a phone (§07): scroll into the preview or the
   * records and the thing you are looking at stays named. Desktop never
   * sticks — the sidebar owns that job there. */
  position: static;
}
.file-detail__title-row { display: flex; align-items: flex-start; gap: var(--sp-3); }
.file-detail__facts { min-height: 24px; }
/* A cell whose meaning is qualified by a tooltip says so to the pointer. */
.file-detail__meta [title] { cursor: help; }

/* ---------------------------------------------------------------------------
 * Usage charts (screen 8c).
 *
 * Server-rendered SVG rather than a charting library: there is no Node in this
 * build, and six bars with a cap line do not need one. The bars use the same
 * cyan as the meters, because a reader who has learned what that colour means
 * on the dashboard should not have to learn it again here.
 *
 * The <svg> stretches to its container with preserveAspectRatio="none", so the
 * bar geometry is computed in a fixed 600×180 space and the browser does the
 * responsive part. Labels live outside the SVG in a flex row aligned to the
 * same column count — text inside a non-uniformly scaled SVG distorts.
 * ------------------------------------------------------------------------ */
.chart {
  display: block;
  width: 100%;
  height: 180px;
  overflow: visible;
}

.chart__bar {
  fill: var(--sky-cyan-fill);
  /* Rounded tops only would need a path; a small uniform radius reads the same
   * at this size and stays one rect. */
  rx: 2;
}

/* The allowance line. Dashed so it cannot be mistaken for a bar, and drawn
 * under them so a month that ran past it visibly crosses the line rather than
 * stopping at it. */
.chart__cap {
  stroke: var(--sky-warn);
  stroke-width: 1;
  stroke-dasharray: 4 4;
  vector-effect: non-scaling-stroke;
}

.chart__labels {
  display: flex;
  justify-content: space-around;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  text-align: center;
}
.chart__labels > * { flex: 1 1 0; }

/* ---------------------------------------------------------------------------
 * Consolidated workspace interactions
 * ------------------------------------------------------------------------ */
.row--center { justify-content: center; }
.row--end { justify-content: flex-end; }

.quota-summary {
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  color: var(--sky-bright);
  text-decoration: none;
}
.quota-summary:hover { background: var(--sky-panel-2); color: var(--sky-bright); }
.upload-primary { width: 100%; }

.files-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--sp-4);
}
.files-header__title { min-width: 0; }

/* The toolbar and the list are wrapped so that one HTMX swap can replace both:
   deleting the last file has to remove the toolbar as well as the rows. The
   wrapper's gap matches .files-workspace's own (and narrows with it at 768px,
   see the breakpoint below). It cannot use `gap: inherit`: its DOM parent is
   .select-scope, a display:contents element, and since gap is not an inherited
   property that resolves to the initial value — which silently glued the
   toolbar to the panel. */
.files-listing {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}
.files-header__actions,
.file-detail__actions,
.files-toolbar__row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.largest-files-list,
.shared-list__body { padding: var(--sp-2); }
/* The file list's rows must line up with the heading row above them, and that
 * row sits in a rowgroup with no side padding — so this one keeps only the
 * vertical half. Horizontal inset comes from each row's own padding. */
.current-folder-items { padding: var(--sp-2) 0; }
.files-empty-drop { box-shadow: none; border: 0; }

.filter-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.filter-chip,
.filter-tab {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--control-h-sm);
  padding-block: 0;
  padding-inline: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-pill);
  color: var(--sky-muted);
  text-decoration: none;
  font: var(--t-ui-weight) var(--t-small-size)/var(--t-line-snug) var(--font-ui);
}
/* Hover is not status. It used to share this declaration with the current tab
 * and so borrowed the action colour to say nothing more than "the pointer is
 * here", which is what standing rule 3 is about. It lifts to the interactive
 * rim instead. */
.filter-chip:hover,
.filter-tab:hover {
  color: var(--sky-bright);
  border-color: var(--sky-edge-2);
  background: color-mix(in srgb, var(--sky-bright) 4%, transparent);
}
/* The current tab keeps cyan: which filter is applied is status, and status is
 * the one thing colour is allowed to carry. */
.filter-tab[aria-current="page"] {
  color: var(--sky-bright);
  border-color: var(--sky-cyan);
  background: color-mix(in srgb, var(--sky-cyan) 8%, transparent);
}
.filter-tabs { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

.drag-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 80;
  padding: var(--sp-5);
  background: color-mix(in srgb, var(--sky-page) 78%, transparent);
  pointer-events: none;
}
.drag-overlay.is-visible { display: grid; place-items: center; }
.drag-overlay__content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  width: min(620px, 100%);
  padding: var(--sp-6);
  border: 2px dashed var(--sky-cyan);
  border-radius: var(--r-panel);
  background: var(--sky-panel);
  box-shadow: var(--shadow-lift);
}

/* Native dialog supplies focus trapping and Escape semantics. The desktop
 * upload surface is a bottom drawer; compact task dialogs stay small. */
.sheet,
.confirm-dialog {
  color: var(--sky-bright);
  border: 1px solid var(--sky-edge);
  background: var(--sky-panel);
  box-shadow: var(--shadow-lift);
}
.sheet::backdrop,
.confirm-dialog::backdrop { background: rgba(3, 7, 11, 0.72); }
.sheet {
  width: min(760px, calc(100vw - 32px));
  max-height: min(760px, calc(100vh - 32px));
  padding: 0;
  border-radius: var(--r-panel);
}
.upload-sheet {
  width: min(920px, 100vw);
  max-height: 72vh;
  margin: auto auto 0;
  border-radius: var(--r-panel) var(--r-panel) 0 0;
  border-bottom: 0;
  overflow: hidden;
}
.compact-sheet { width: min(520px, calc(100vw - 32px)); }
.sheet__surface {
  display: flex;
  flex-direction: column;
  max-height: inherit;
  background: var(--sky-panel);
}
.sheet__head,
.sheet__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4);
  background: var(--sky-panel);
}
.sheet__head { border-bottom: 1px solid var(--sky-edge); }
.sheet__body { min-height: 0; overflow-y: auto; padding: var(--sp-4); }
.sheet__actions {
  justify-content: flex-end;
  border-top: 1px solid var(--sky-edge);
  padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom));
}
/* A standing note, not a trailing paragraph: it says what happens to uploads
 * when the page closes, which is worth reading before the list is scrolled to
 * the end rather than after. It lives outside .sheet__body so it does not
 * scroll with the queue, and it takes over the separator the action row would
 * otherwise draw against the body. */
.sheet__note {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--sky-edge);
  background: var(--sky-panel);
}
.sheet__note + .sheet__actions { 
  border-top: 0;
  padding-top: 0;
 }
.sheet__sticky-action {
  position: sticky;
  bottom: calc(-1 * var(--sp-4));
  display: flex;
  justify-content: flex-end;
  margin: 0 calc(-1 * var(--sp-4)) calc(-1 * var(--sp-4));
  padding: var(--sp-4);
  border-top: 1px solid var(--sky-edge);
  background: var(--sky-panel);
}
/* flex:none because .sheet__body is a flex column with a bounded height, and
 * the min-height below replaces the automatic min-content floor that would
 * otherwise stop this from shrinking. Without it the queue is squashed to the
 * space left over — 390px for 571px of rows at six files — and, being
 * overflow:visible, paints the surplus straight over whatever follows it.
 * The body is the scrolling region; the list inside it keeps its full height. */
.upload-queue {
  flex: none;
  min-height: 84px;
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
}
.confirm-dialog {
  width: min(460px, calc(100vw - 32px));
  padding: var(--sp-5);
  border-radius: var(--r-panel);
}

/* Row actions are always visible. They used to be opacity:0 until hover, which
 * is undiscoverable with a mouse and unreachable without one. */
.filerow__actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); }
/* The folder row's chevron is a real link, not decoration, so it needs a
 * target rather than a glyph's worth of pixels. Sized on --control-h-sm like
 * the row's other controls, which promotes to the 44px tap floor at 768. */
.filerow__into {
  display: inline-grid;
  place-items: center;
  min-height: var(--control-h-sm);
  width: var(--control-h-sm);
  border-radius: var(--r-control);
  color: var(--sky-muted);
  text-decoration: none;
  transition: background 140ms ease, color 140ms ease;
}
.filerow__into:hover { color: var(--sky-bright); background: var(--sky-panel-2); }

/* The menu was a <details>. It is a button and a panel now, because a details
 * element closes on neither an outside click nor Escape and announces itself as
 * a disclosure rather than a menu — see Alpine.data('rowMenu') in app.js. The
 * summary rules that used to sit here went with it. */
.action-menu { position: relative; }
.action-menu__scrim { display: none; }

.action-menu__panel {
  position: absolute;
  top: calc(100% + var(--sp-1));
  right: 0;
  z-index: 30;
  display: grid;
  min-width: 150px;
  padding: var(--sp-1);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel);
  box-shadow: var(--shadow-lift);
}
.action-menu__panel button,
.action-menu__panel a {
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--sky-bright);
  text-align: left;
  text-decoration: none;
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-snug) var(--font-ui);
  cursor: pointer;
  /* The gap an icon needs, on every item. It is set here rather than beside
   * the display below because gap is inert until something is a flex container,
   * so the touch items pick it up from their own display:flex at 768. */
  gap: var(--sp-2);
}
/* :not(.action-menu__touch) is load-bearing. .action-menu__touch is hidden at
 * file scope and shown only at 768, and `.action-menu__panel a` (0,1,1) out-
 * specifies `.action-menu__touch` (0,1,0) — so declaring display here without
 * the exclusion would light up the phone-only Download and Share on desktop. */
.action-menu__panel button:not(.action-menu__touch),
.action-menu__panel a:not(.action-menu__touch) {
  display: flex;
  align-items: center;
}
.action-menu__panel button:hover,
.action-menu__panel button:focus-visible,
.action-menu__panel a:hover,
.action-menu__panel a:focus-visible { background: var(--sky-panel-2); }
.action-menu__touch { display: none; }

.file-detail { max-width: 980px; }
.file-detail__title h1 { overflow-wrap: anywhere; }
.file-detail__meta {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-3) var(--sp-4);
}
.file-detail__meta > div { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.file-detail__meta dt { color: var(--sky-faint); }
.file-detail__meta dd { margin: 0; color: var(--sky-bright); overflow-wrap: anywhere; }
.inline-rename { display: flex; gap: var(--sp-2); align-items: center; }
.inline-rename .input-sky { flex: 1; }
.disclosure,
.danger-disclosure {
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  padding: var(--sp-3);
}
.disclosure > summary,
.danger-disclosure > summary { cursor: pointer; }
.disclosure__body { margin-top: var(--sp-4); }
.share-summary {
  padding: var(--sp-3);
  border-left: 3px solid var(--sky-cyan);
  background: color-mix(in srgb, var(--sky-cyan) 8%, transparent);
}

.target-picker { display: grid; gap: var(--sp-2); }
.target-picker__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 48px;
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: transparent;
  color: var(--sky-bright);
  text-align: left;
  cursor: pointer;
}
/* A row you can pick, not an action: the rim lifts, the fill carries the
 * pointer, and cyan stays out of it. */
.target-picker__row:hover,
.target-picker__row:focus-visible { border-color: var(--sky-edge-2); background: var(--sky-panel-2); }
.shared-list__archive-title { margin: var(--sp-5) var(--sp-3) var(--sp-2); }

.plan-page { max-width: 1060px; }
/* Sticky: it masks the list scrolling under it, so it has to be the page. */
.plan-nav { display: flex; gap: var(--sp-2); position: sticky; top: 0; z-index: 20; padding: var(--sp-2) 0; background: var(--sky-page); }
/* A nav link, not a control — it keeps its own 44px. The padding used to be
 * 10px 14px against 16px/1.6 inherited text, which came to 45.59px and so
 * overflowed the min-height it was supposed to sit inside. Typed at the ui
 * step on a flat line, the content is 14.5px and the min-height owns the
 * height again. */
.plan-nav a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding: var(--sp-2) var(--sp-3);
  font: var(--t-ui-weight) var(--t-ui-size)/var(--t-line-flat) var(--font-ui);
  color: var(--sky-muted);
  text-decoration: none;
}
.plan-nav a:hover { color: var(--sky-bright); }
.plan-section { scroll-margin-top: 64px; }
.plan-section__summary { display: none; }
.plan-section__content { padding-top: var(--sp-2); }
.charges-table { min-width: 680px; }
.charges-table th.num,
.charges-table td.num { text-align: right; }
.charges-pager { border-top: 1px solid var(--sky-edge); border-bottom: 0; }

.account-page { max-width: 760px; }
.account-summary { cursor: pointer; list-style: none; }
.account-summary::-webkit-details-marker { display: none; }
.account-summary .btn { pointer-events: none; }
.session-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 52px;
}
.session-row--current { padding-bottom: var(--sp-3); border-bottom: 1px solid var(--sky-edge); }

@media (max-width: 768px) {
  /* clip rather than hidden: both stop sideways scroll, but hidden turns the
   * body into a scroll container, which silently disables every
   * position:sticky inside it — including the detail page's name bar. Old
   * browsers fall back to the first declaration and lose only the sticky. */
  html, body { max-width: 100%; overflow-x: hidden; overflow-x: clip; }
  /* The tap floor, named once. Every control reads --control-h, so promoting
   * the token here promotes all of them — instead of eleven rules each
   * claiming 44px separately and drifting apart. */
  :root {
    --control-h: var(--control-h-touch);
    --control-h-sm: var(--control-h-touch);
  }

  .topbar { flex-wrap: wrap; gap: var(--sp-3); }
  .topbar__links {
    flex: 1 1 100%;
    min-width: 0;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: var(--sp-3);
  }

  .checkout-pending { box-sizing: border-box; width: 100%; padding-inline: var(--sp-3); }
  .checkout-pending .panel-sky { box-sizing: border-box; width: 100%; min-width: 0; }
  .checkout-pending .panel-sky__head,
  .checkout-pending .panel-sky__body { min-width: 0; padding-inline: var(--sp-4); }
  .checkout-pending .table-sky { table-layout: fixed; }
  .checkout-pending .table-sky td { overflow-wrap: anywhere; }

  .shell__main { min-width: 0; padding-inline: var(--sp-3); }
  /* With Upload gone from here, New folder fits beside the title instead of
   * taking a row of its own. */
  .files-header { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .files-header__actions { width: auto; }
  /* Upload is the centre of the tab bar at this width. A second, larger copy
   * of it above the list costs a file's worth of screen to say the same thing. */
  .files-header__actions .btn--primary { display: none; }

  /* 32px between every block is a desktop rhythm. On a phone each of those
   * gaps is half a file. */
  .files-workspace,
  .files-listing { gap: var(--sp-4); }
  .grouprow { padding: var(--sp-2) var(--sp-3) 2px; }
  /* One line, not two. Every row of chrome here is a file you cannot see. */
  .files-toolbar__row { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
  .files-search { grid-column: 1; width: 100%; }
  .files-filter-button { grid-column: 2; min-width: 0; padding-inline: var(--sp-3); }
  .filter-sort-grid { grid-template-columns: 1fr; }

  /* Two lines, always. Name on the first, everything else on the second,
   * indented to sit under the name. The old layout gave the size, the date and
   * a full-width button a line each and came to 187px a row — two files on a
   * phone screen.
   *
   * The first column is an empty 28px, which is the chip's width: it is what
   * indents the second line under the name without nesting anything. */
  .filerow {
    grid-template-columns: 28px auto auto auto minmax(0, 1fr) 44px;
    grid-template-rows: auto auto;
    column-gap: var(--sp-2);
    row-gap: 3px;
    align-items: center;
    min-width: 0;
    padding: var(--sp-2) var(--sp-3);
  }
  /* Five columns of headings do not fit here. Sorting stays in the filter
   * sheet at this width, which is why those selects are kept. */
  .filerow--head { display: none; }
  .filter-quick { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
  /* The active chip takes the accent fill the proposal draws (.mk-chip[data-on]).
   * Cyan on a filter is action colour on an action, not a type colour. */
  .filter-quick a[aria-current="page"] {
    background: var(--sky-cyan);
    border-color: var(--sky-cyan);
    color: var(--sky-action-ink);
  }

  /* The public list has four tracks, not six, so its second line places the two
   * cells it does have and its actions take the last column. Without this the
   * owner's placements above would leave gaps where Status and Link are not. */
  .filelist--public .filerow {
    grid-template-columns: 28px auto minmax(0, 1fr) 44px;
  }
  .filelist--public .filerow__ident { grid-column: 1 / 4; }
  .filelist--public .filerow__size { grid-column: 2; }
  .filelist--public .filerow__date { grid-column: 3; }
  .filelist--public .filerow__actions { grid-column: 4; }
  /* The owner's rows hide their buttons here and move everything into the row
   * menu, because a phone has no hover. This list has no menu — a recipient's
   * two actions ARE the row — so the buttons stay and go icon-only instead.
   * Each anchor carries an aria-label naming the file, so what is dropped is
   * the repeated word, not the meaning. Measured before this was written: two
   * labelled buttons crushed the cell to 44px and both buttons to zero. */
  .filelist--public .filerow__actions > .btn { display: inline-flex; padding-inline: var(--sp-2); }
  .filelist--public .filerow__actions > .btn span { display: none; }

  .filerow__ident { grid-column: 1 / 6; grid-row: 1; gap: var(--sp-2); }
  .filerow__status { grid-column: 2; grid-row: 2; }
  .filerow__size { grid-column: 3; grid-row: 2; }
  .filerow__date { grid-column: 4; grid-row: 2; }
  .filerow__link { grid-column: 5; grid-row: 2; display: flex; align-items: center; }
  .filerow__actions { grid-column: 6; grid-row: 1 / 3; justify-content: flex-end; }
  /* The slug is too long for a phone's second line; the icon alone says the
   * file is exposed, and the accessible name still says how many. */
  .filerow__slug span { display: none; }

  .filerow__name { font-size: var(--t-ui-size); }
  /* These cells are spans, so left inline their height comes from line-height
   * rather than from what is in them — which is what kept the second line
   * 26px tall while holding a 5px dot. */
  .filerow__status,
  .filerow__size,
  .filerow__date {
    display: flex;
    align-items: center;
    line-height: 1;
  }
  .filerow__size, .filerow__date { font-size: var(--t-meta-size); }
  /* A folder's size is what its whole subtree holds. It is worth the width on
   * desktop, but at this one the item count on the line above already says
   * enough and the row has two lines to spend. */
  .filerow--folder .filerow__size { display: none; }
  /* The chip already says what the file is, so the type line is only worth its
   * height when the system has something to say — an explanation, or where a
   * search found it. */
  .filerow--plain .filerow__meta { display: none; }
  .filerow__meta { white-space: normal; font-size: var(--t-meta-size); }

  .typechip { width: 28px; height: 28px; border-radius: 8px; }
  .typechip svg { width: 11px; height: 11px; }
  .typechip em { font-size: 6.5px; }

  /* The status pill becomes a dot on its own line. "Available" on every row is
   * noise a colour already carries, so the word is hidden from the eye and
   * kept for a screen reader — the other states still say what they are,
   * because those are the ones worth reading. */
  .filerow__status .pill {
    border: 0;
    background: none;
    padding: 0;
    gap: 5px;
    font-size: var(--t-meta-size);
    /* The .pill shorthand sets line-height 1.4. Left alone, a row grew by 4px
     * the moment a scan finished and the word appeared — the list shifting
     * under your thumb is exactly what a settling scan should not do. */
    line-height: 1;
  }
  .filerow__status .pill::before {
    content: "";
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 auto;
  }
  .filerow__status .pill--available { font-size: 0; gap: 0; }

  /* One target, not three. Download and Share move into the menu, which is the
   * only thing a phone can open — there is no hover to reveal them. */
  .filerow__actions > .btn { display: none; }
  /* The three rules that used to live here — a 44px box, a ::after carrying
   * "\22EF", and a min-width/min-height pair — are gone. .iconbtn is the box
   * now, and --control-h promotes to the 44px tap floor at this width, so
   * every one of them was restating something the token system already says.
   * The glyph is markup rather than a pseudo-element: icons phase 3. */
  .action-menu__scrim {
    display: block;
    position: fixed;
    inset: 0;
    /* 41/42, not the 39/40 G6.1 writes. .tabbar is already z-index 40 and
     * paints later, so at 40 the sheet went UNDER it: Delete sat at bottom 799
     * against a tab bar starting at 751, and elementFromPoint over the Delete
     * row returned the tab bar's link. The action was unreachable by tap.
     * Above the bar, the scrim also dims it, which is what a modal sheet
     * should do — you cannot navigate away underneath one. */
    z-index: 41;
    background: rgba(0, 0, 0, 0.45);
    animation: mo-scrim-in 200ms ease-out;
  }
  /* At this width the panel is a sheet: docked to the bottom edge, full
   * width, 48px rows (§06). Alpine still owns it end to end, so the
   * two-systems rule holds — nothing here came from the server. */
  .action-menu__panel {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 42;
    min-width: 0;
    border-radius: var(--r-panel) var(--r-panel) 0 0;
    padding: var(--sp-3) var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-bottom));
  }
  /* 48px replaces the 44px the style-guide plan left here for this phase. */
  .action-menu__panel button,
  .action-menu__panel a { min-height: 48px; }
  /* The transition classes keep their names; what they do changes. */
  .mo-pop-in    { transition: opacity 260ms var(--sky-ease-sheet), transform 260ms var(--sky-ease-sheet); }
  .mo-pop-start { opacity: 1; transform: translateY(100%); }
  .mo-drop-out  { transition: opacity 200ms ease-in, transform 200ms ease-in; }
  .mo-dropped   { opacity: 1; transform: translateY(100%); }

  .action-menu__panel .action-menu__touch { display: flex; align-items: center; }
  .filerow__progress { grid-row: 5; }

  /* Selection, mobile half. A checkbox on every row has no width to spare in
   * the two-line layout above until the reader has actually asked to select
   * something, so it stays hidden until .select-toggle sets is-selecting on
   * .select-scope — see the Select button in the files header. */
  .select-toggle { display: inline-flex; }
  .filerow__select { display: none; }
  .is-selecting .filerow__select { display: inline-flex; }

  /* The recipient's list is the exception, and it is a width argument rather
   * than a different opinion: it carries no status pill, no share-link column
   * and no row menu, so the checkbox has room the owner's row does not have.
   * Measured at 390px — a 24px box, the name ending at 304 and the actions
   * starting at 312. Hiding it here would mean the visitor on a phone has no
   * way to select at all, since this page has no Select toggle to reveal it. */
  .filelist--public .filerow__select { display: inline-flex; }

  /* fileListHeader is display:none at this width (five columns do not fit),
   * so there is no heading row for the bar to swap into. It docks to the
   * viewport instead, clear of the tab bar's own fixed footprint — 8px
   * padding + a 44px row + 8px padding, from the .tabbar rules above. */
  .selection-bar {
    position: fixed;
    inset: auto var(--sp-3) calc(var(--tabbar-h) + env(safe-area-inset-bottom) + var(--sp-3)) var(--sp-3);
    z-index: 41;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-2);
    border-radius: var(--r-panel);
    border: 1px solid var(--sky-edge);
    box-shadow: var(--shadow-lift);
  }
  .selection-bar__actions { flex-wrap: wrap; }
  .selection-bar__actions .btn { flex: 1 1 auto; }

  .sheet,
  .upload-sheet,
  .compact-sheet,
  .target-sheet,
  .share-sheet {
    inset: 0;
    width: 100vw;
    max-width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border: 0;
    border-radius: 0;
  }
  /* At this width these are sheets, not dialogs — §08 gives a sheet 260ms on
   * the sheet curve. Above the breakpoint they keep the 160ms dialog rise. */
  .sheet[open],
  .compact-sheet[open],
  .share-sheet[open] { animation: mo-sheet-up 260ms var(--sky-ease-sheet); }

  .sheet__surface { height: 100%; max-height: 100%; }
  .sheet__body { flex: 1; }
  .sheet__sticky-action .btn { width: 100%; }

  /* The preview card bleeds to the phone's edges — the stage is the widest
   * thing on the page and inset chrome would shrink the file for nothing.
   * Its height comes from its own aspect ratio now, not a rule here. */
  .file-detail__preview { margin-inline: calc(-1 * var(--sp-3)); border-radius: 0; }
  .file-detail__actions { display: grid; grid-template-columns: 1fr 1fr; }
  /* Download is the reason most people opened this page: full width, above
   * Share, before any record block. */
  .file-detail__actions .btn--primary { grid-column: 1 / -1; }
  /* Name, status and the actions row stay named while the records scroll. */
  .file-detail__top {
    position: sticky;
    top: 0;
    z-index: 30;
    margin-inline: calc(-1 * var(--sp-3));
    padding: var(--sp-2) var(--sp-3);
    background: color-mix(in srgb, var(--sky-page) 92%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border-bottom: 1px solid var(--sky-edge);
  }
  .file-detail__meta { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .inline-rename { flex-wrap: wrap; }

  .plan-nav { display: none; }
  .plan-section {
    border: 1px solid var(--sky-edge);
    border-radius: var(--r-panel);
    background: var(--sky-panel);
  }
  .plan-section__summary { display: block; padding: var(--sp-4); cursor: pointer; }
  .plan-section__content { padding: 0 var(--sp-3) var(--sp-3); }
  .quota-grid { grid-template-columns: 1fr; }
  .account-summary { align-items: center; }
  .session-row { align-items: flex-start; }
}

/* ---------------------------------------------------------------------------
 * Style guide (staff only).
 *
 * A swatch takes its colour from the token itself, never from an inline style:
 * the CSP forbids one, and a hex repeated here would drift from the palette the
 * moment either changed. Same reasoning for the spacing bars, which are drawn
 * at the token's true width rather than at a number copied beside it.
 * ------------------------------------------------------------------------ */
.styleguide__section { padding-block: var(--sp-4); border-top: 1px solid var(--sky-edge); }
.styleguide__swatches {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--sp-3);
}
.styleguide__swatch { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.styleguide__chip {
  flex: 0 0 auto;
  width: var(--control-h-sm);
  height: var(--control-h-sm);
  border-radius: var(--r-control);
  border: 1px solid var(--sky-edge);
}
.styleguide__row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
/* A specimen the guide draws itself, because the class it shows has no
 * component behind it. .is-hover is added to the real :hover selector rather
 * than restated here, so the demo cannot show a hover the app does not have. */
.styleguide__demo { padding: var(--sp-3); border: 1px solid var(--sky-edge); border-radius: var(--r-control); }
.styleguide__bar { height: var(--sp-3); border-radius: var(--r-pill); background: var(--sky-cyan); }

.styleguide__chip--sky-page { background: var(--sky-page); }
.styleguide__chip--sky-deep { background: var(--sky-deep); }
.styleguide__chip--sky-panel { background: var(--sky-panel); }
.styleguide__chip--sky-panel-2 { background: var(--sky-panel-2); }
.styleguide__chip--sky-edge { background: var(--sky-edge); }
.styleguide__chip--sky-edge-2 { background: var(--sky-edge-2); }
.styleguide__chip--sky-bright { background: var(--sky-bright); }
.styleguide__chip--sky-muted { background: var(--sky-muted); }
.styleguide__chip--sky-faint { background: var(--sky-faint); }
.styleguide__chip--sky-cyan { background: var(--sky-cyan); }
.styleguide__chip--sky-cyan-text { background: var(--sky-cyan-text); }
.styleguide__chip--sky-cyan-fill { background: var(--sky-cyan-fill); }
.styleguide__chip--sky-action-ink { background: var(--sky-action-ink); }
.styleguide__chip--sky-action-hover { background: var(--sky-action-hover); }
.styleguide__chip--sky-go { background: var(--sky-go); }
.styleguide__chip--sky-warn { background: var(--sky-warn); }
.styleguide__chip--sky-stop { background: var(--sky-stop); }
.styleguide__chip--sky-acted { background: var(--sky-acted); }

.styleguide__bar--sp-1 { width: var(--sp-1); }
.styleguide__bar--sp-2 { width: var(--sp-2); }
.styleguide__bar--sp-3 { width: var(--sp-3); }
.styleguide__bar--sp-4 { width: var(--sp-4); }
.styleguide__bar--sp-5 { width: var(--sp-5); }
.styleguide__bar--sp-6 { width: var(--sp-6); }
.styleguide__embed-frame {
  width: min(100%, 45rem);
  aspect-ratio: 8 / 5;
  overflow: hidden;
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
  background: #050709;
}
.styleguide__embed {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}


/* Controlled public media embed. The branded chrome is a separate grid row:
 * it stays visible without competing with the media or VideoJS controls. */
.embed-player {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  width: 100vw;
  height: 100vh;
  margin: 0;
  background: #050709;
  color: #f6f8fb;
  overflow: hidden;
}
.embed-player--denied { grid-template-rows: minmax(0, 1fr); }
.embed-player__chrome {
  z-index: 1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
  padding: .45rem var(--sp-3);
  border-bottom: 1px solid rgb(255 255 255 / 10%);
  background: #0d1118;
}
.embed-player__brand {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  color: #fff;
  font: var(--t-meta-weight) var(--t-meta-size)/var(--t-meta-line) var(--font-ui);
  text-decoration: none;
}
.embed-player__brand:hover,
.embed-player__brand:focus-visible { color: var(--sky-cyan); }
.embed-player__brand-dot {
  width: .55rem;
  height: .55rem;
  border-radius: 50%;
  background: var(--sky-cyan);
  box-shadow: 0 0 0 .2rem rgb(49 210 242 / 14%);
}
.embed-player__title {
  min-width: 0;
  margin: 0;
  overflow: hidden;
  color: rgb(246 248 251 / 68%);
  font: var(--t-meta-weight) var(--t-meta-size)/var(--t-meta-line) var(--font-ui);
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.embed-player__stage {
  position: relative;
  display: grid;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  place-items: center;
  overflow: hidden;
}
.embed-player__media {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.embed-player__video,
.embed-player__video > video-skin {
  display: block;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
}
.embed-player__audio-wrap {
  display: grid;
  width: min(92%, 38rem);
  gap: var(--sp-3);
  justify-items: center;
}
.embed-player__audio-mark {
  display: grid;
  width: 4rem;
  height: 4rem;
  place-items: center;
  border-radius: 1.25rem;
  background: var(--sky-cyan);
  color: var(--sky-action-ink);
  font: var(--t-h2-weight) var(--t-h2-size)/var(--t-line-flat) var(--font-ui);
}
.embed-player__name {
  width: 100%;
  margin: 0;
  overflow: hidden;
  color: #f6f8fb;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.embed-player__audio { width: 100%; }
.embed-player__denied {
  display: grid;
  width: min(90%, 28rem);
  gap: var(--sp-3);
  justify-items: center;
  padding: var(--sp-5);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-panel);
  background: var(--sky-panel);
  text-align: center;
}
.embed-player__denied h1,
.embed-player__denied p { margin: 0; }

.embed-code {
  max-height: 8rem;
  margin: 0;
  padding: var(--sp-3);
  overflow: auto;
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel-2);
  color: var(--sky-bright);
  font: var(--t-small-weight) var(--t-small-size)/var(--t-small-line) var(--font-mono);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/**
* Video.js specific
*/
media-poster {
  display: none;
}

/**
 * Analytics page.
 */
.analytics-controls { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.analytics-controls .input-sky { width: auto; min-width: 9rem; }
.analytics-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-2);
}
.analytics-stat {
  display: grid;
  gap: .2rem;
  padding: var(--sp-3);
  border: 1px solid var(--sky-edge);
  border-radius: var(--r-control);
  background: var(--sky-panel-2);
}
.analytics-chart { position: relative; width: 100%; height: 20rem; }
.analytics-breakdowns { align-items: start; }
@media (max-width: 768px) {
  .analytics-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .analytics-chart { height: 16rem; }
}
