/* ============================================================================
   App shell — the logged-in surfaces (workspace, brand, settings).

   Every rule is scoped under html.es-shell-v2, the class index.php splices in only
   when APP_SHELL_SIDEBAR=1. With the flag off this file contributes nothing, which
   is what makes rollback an env-var flip rather than a redeploy.

   Visibility and the content offset live in the inline #route-boot-styles block in
   index.html, not here — they must apply on the first painted frame, before this
   stylesheet has necessarily arrived. This file is presentation only.

   Geometry note: the sidebar never reads --header-height or --editor-content-offset.
   The editor's sticky right panel depends on those, and the two layouts must not be
   able to disturb each other.
   ============================================================================ */

/* ---------------------------------------------------------------- sidebar --- */
#app-sidebar {
  position: fixed;
  inset-block: 0;
  left: 0;
  /* Above .site-footer (z-index 100 in marketing.css). The footer is full-bleed, so
     without this it draws over the bottom of the rail. It is also offset below, but
     the stacking order still has to be right for any other full-width band. */
  z-index: 120;
  width: var(--sidebar-width);
  flex-direction: column;
  /* Equal top and bottom: the logo and the account row are the rail's two end-stops,
     so an uneven inset there is visible even when nothing else is.
     Left is set against the panel edge, not the rail's own: the row's right-hand gap
     is padding-right plus the 14px panel gutter, so an equal padding on both sides
     reads as crowded-left. 20/10 lands 20 left against 24 right — deliberately a
     hair left of centre, which is where it looks right rather than measures right.
     Bottom runs 8px tighter than top because the two end-stops are not built the
     same: the logo is flush, while the account row carries 10px of its own padding.
     Matching the numbers left the row's visible box sitting 32px off the floor
     against the logo's 22px, which read as floating. 14 + 10 lands it at 24. */
  padding: 22px 10px 14px 20px;
  /* Transparent: the rail is part of the frame, sitting directly on --app-shell-bg.
     The gutter around the content panel provides the separation, so no border is
     needed — a border here plus a panel edge would be two dividers doing one job. */
  background: none;
  overflow: hidden;
}

/* The real wordmark asset, same file the header uses — not a redrawn substitute. */
.shell-logo {
  display: flex;
  align-items: center;
  padding: 2px 10px 18px;
  flex: 0 0 auto;
}
.shell-logo .brand-wordmark {
  height: 32px;
  width: auto;
  display: block;
}

/* The single entry point for starting work — and with the standing drop box gone, the
   only one. Sized to match that responsibility: a 38px button cannot be the primary
   action of the whole app. Taller, heavier, with a lifted shadow so it reads as the
   one thing on this rail you press. */
.shell-new-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-xs);
  flex: 0 0 auto;
  margin-bottom: 18px;
  /* 42px: the original 38 was too slight for the app's only entry point, 47 was
     chunky. A touch more presence than a standard button, not a slab. */
  padding: 12px;
  border-radius: var(--radius-md);
  background: var(--primary);
  color: #fff;
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.24);
  transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.shell-new-btn:hover {
  background: var(--primary-dark);
  box-shadow: 0 4px 14px rgba(var(--primary-rgb), 0.36);
}
.shell-new-btn:active { transform: translateY(1px); }
.shell-new-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.35);
}
.shell-new-btn svg { flex-shrink: 0; }

/* Rows sit further apart than a dense nav would: this rail holds seven or eight things
   in a 280px column with room to spare, so crowding them buys nothing. */
.shell-nav { flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px; }

.shell-link,
.shell-brand-link {
  display: flex;
  align-items: center;
  gap: 10px;
  /* flex:0 0 auto is load-bearing: these sit in a flex column that scrolls, so without
     it the rows shrink to fit instead of overflowing — a 20-brand rail collapsed its
     rows from 30px to 17px rather than scrolling. */
  flex: 0 0 auto;
  height: var(--sidebar-row-height);
  padding: 0 10px;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  color: var(--text-tertiary);
  text-decoration: none;
  cursor: pointer;
}
/* Brand rows run one step shorter than the nav rows above them. At a matched 38px the
   rail read as one long column of equal-weight rows, with only a small grey label to
   say that brands sit *under* Workspace and Team rather than beside them — and a label
   is a weak signal against a strong rhythm. The 4px gaps are already tight; height is
   what carries the hierarchy. It also halves the jump into the condensed list (34 -> 30
   instead of 38 -> 30), so crossing eight brands stops being a visible lurch. */
.shell-brand-link { height: var(--sidebar-brand-row-height); }

.shell-link { font-weight: var(--weight-semibold); }
.shell-link svg { color: var(--text-tertiary); flex-shrink: 0; }
/* Clear, quick feedback — the previous cream-on-cream hover was almost invisible
   against the flat canvas. */
.shell-link,
.shell-brand-link { transition: background 0.12s ease, color 0.12s ease; }
.shell-link:hover,
.shell-brand-link:hover { background: var(--app-shell-hover); color: var(--text-primary); }
.shell-link:hover svg { color: var(--text-secondary); }
.shell-link:focus-visible,
.shell-brand-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.4);
}
.shell-link.is-active { background: var(--primary-subtle); color: var(--primary-text); }
.shell-link.is-active svg { color: var(--primary-text); }

.shell-group {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 24px 0 8px;
  padding: 0 10px;
}
.shell-group span {
  font-size: var(--text-2xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: var(--weight-semibold);
  color: var(--text-tertiary);
}
.shell-group button {
  border: 0;
  background: none;
  color: var(--text-tertiary);
  font-size: var(--text-lg);
  line-height: 1;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.shell-group button:hover { background: var(--app-shell-hover); color: var(--text-secondary); }

/* The rail scrolls; the foot stays pinned. flex:1 + min-height:0 is what makes the
   overflow work inside a flex column — without min-height:0 the list refuses to
   shrink and pushes the foot off-screen. */
.shell-brands {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0 -4px;
  padding: 0 4px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* A touch heavier than body text: these are names, and at 400 they read lighter than
   the nav rows directly above them. */
.shell-brand-link { color: var(--text-secondary); font-weight: var(--weight-medium); }
.shell-brand-link.is-active {
  background: var(--primary-subtle);
  color: var(--primary-text);
  font-weight: var(--weight-semibold);
}
/* A small rounded initial tile rather than a saturated dot — quieter beside the
   nav icons, and legible at rail size where most logos would not be. */
.shell-brand-mark {
  width: 18px;
  height: 18px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  font-weight: var(--weight-bold);
  color: #fff;
  letter-spacing: 0.02em;
}
.shell-brand-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shell-brand-count {
  margin-left: auto;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
/* --- Condensed mode: >8 brands ------------------------------------------------
   An agency with twenty brands needs to see as many as possible before scrolling, so
   rows tighten and the marks shrink. Below the threshold the rail has room to spare
   and stays comfortable — the same list, sized to the person using it. */
.shell-brands.is-condensed { gap: 1px; }
.shell-brands.is-condensed .shell-brand-link {
  height: 30px;
  font-size: var(--text-sm);
  gap: 8px;
}
.shell-brands.is-condensed .shell-brand-mark {
  width: 15px;
  height: 15px;
  font-size: 8px;
}
.shell-brands.is-condensed .shell-brand-count { font-size: var(--text-2xs); }

/* The full-width add row replaces the header "+" when the list is short: there is room
   for it, and a row is a far bigger target than a 16px glyph. Quieter than a real
   brand so it never reads as one. */
/* Same size and weight as a real brand row — it sits in the same list and a 1px
   difference reads as a mistake, not as emphasis. Colour alone does the de-emphasis. */
.shell-brand-link.is-add {
  color: var(--text-tertiary);
  font-weight: var(--weight-medium);
}
.shell-brand-link.is-add:hover { color: var(--primary-text); }
.shell-brand-link.is-add .shell-brand-mark {
  background: none;
  color: var(--text-tertiary);
  font-size: var(--text-base);
  font-weight: var(--weight-normal);
}
/* On /brand/new this row IS the current page, so it takes the same highlight as a
   brand row. Restated here because .is-add's de-emphasised colour is authored after
   .is-active and would otherwise win at equal specificity. */
.shell-brand-link.is-add.is-active,
.shell-brand-link.is-add.is-active .shell-brand-mark {
  color: var(--primary-text);
}
.shell-brand-link.is-add.is-active { font-weight: var(--weight-semibold); }

/* --- foot: two blocks, not three loose rows --- */
/* The rail ran 18/18/18 down its top half and 5/10/8/12 down its bottom half. Same
   scale on both sides of the divider so the whole column reads as one rhythm. */
.shell-foot {
  flex: 0 0 auto;
  margin-top: var(--spacing-md);
  padding-top: var(--spacing-md);
  border-top: 1px solid var(--border);
}

.shell-meter {
  /* Warm card fill: this sits on the white rail, where the cool canvas read as a
     grey patch against warm hovers on the rows directly above it. */
  background: var(--surface-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 11px 12px;
  margin-bottom: var(--spacing-xs);
}
.shell-meter[hidden] { display: none; }
.shell-meter-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 7px;
}
.shell-meter-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
}
.shell-meter-val {
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}
.shell-meter-val b { color: var(--text-primary); font-weight: var(--weight-bold); }
.shell-meter-track {
  height: 5px;
  border-radius: var(--radius-pill);
  /* Was --bg-tertiary — the last cream left in the rail, sitting inches from rows
     that hover with --surface-wash. */
  background: var(--surface-wash);
  overflow: hidden;
}
.shell-meter-fill {
  height: 100%;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--primary);
  transition: width 0.3s ease;
}
.shell-meter-fill.is-warning { background: var(--warning-icon); }
.shell-meter-fill.is-full { background: var(--danger); }
.shell-meter-foot {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  margin-top: 7px;
  font-size: var(--text-2xs);
  color: var(--text-tertiary);
}
.shell-meter-foot a {
  color: var(--primary-text);
  font-weight: var(--weight-semibold);
  text-decoration: none;
}
.shell-meter-foot a:hover { text-decoration: underline; }

/* --- upsell: the one promotional block in the app shell --------------------- */
/* Tinted rather than white so the offer is distinguishable from the quota above it,
   but built from --primary at a single low alpha — a gradient and a solid PRO badge
   both read as an ad pasted into the rail rather than a part of it. Corner ladder:
   block (12), CTA inside it control (10). */
.shell-upsell {
  padding: var(--spacing-sm);
  border: 1px solid rgba(var(--primary-rgb), 0.16);
  border-radius: var(--radius-lg);
  background: rgba(var(--primary-rgb), 0.05);
  margin-bottom: var(--spacing-xs);
}
.shell-upsell[hidden] { display: none; }

/* Joined to the meter when both are up: one block reading number → hairline → offer,
   rather than two cards with a seam between them. The meter keeps its own bottom
   border and that becomes the divider, so the join is one hairline, not two. The
   :has() is what keeps this honest — a team owner has no personal meter, and this
   card has to stand on its own there. */
.shell-meter:has(+ .shell-upsell:not([hidden])) {
  margin-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.shell-meter:not([hidden]) + .shell-upsell {
  border-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.shell-upsell-title {
  margin: 0 0 9px;
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}

.shell-upsell-features {
  list-style: none;
  margin: 0 0 var(--spacing-sm);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.shell-upsell-features li {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--text-secondary);
}
/* Matches the Settings upsell's checklist rather than inventing a second tick. */
.shell-upsell-features li::before {
  content: '\2713';
  color: var(--primary);
  font-weight: var(--weight-bold);
  flex-shrink: 0;
}

/* Full-width and solid: in a 260px rail a right-aligned button would leave a stripe
   of dead tint beside it, and this is the card's only action. */
.shell-upsell-cta {
  display: block;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  border-radius: var(--radius-md);
  background: var(--primary);
  color: var(--surface-white);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  cursor: pointer;
  transition: background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.shell-upsell-cta:hover {
  background: var(--primary-dark);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}
.shell-upsell-cta:active { transform: translateY(0); box-shadow: none; }
.shell-upsell-cta:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.28);
}

.shell-upsell-note {
  margin: 7px 0 0;
  text-align: center;
  font-size: var(--text-2xs);
  color: var(--text-tertiary);
}

/* A paying plan below the top tier is being offered volume, not features — so the
   checklist goes and what is left is one line and a button. Same element, so nothing
   has to know which variant is on screen. */
.shell-upsell--compact .shell-upsell-features { display: none; }
.shell-upsell--compact .shell-upsell-title {
  font-size: var(--text-sm);
  margin-bottom: var(--spacing-xs);
}

/* The rail is a fixed column with overflow:hidden, and .shell-foot cannot shrink — so
   on a short window something has to give or the account row is clipped off the bottom.
   The card sheds its parts in the order it can afford to lose them: the price and trust
   line first, then the checklist, then the headline. The button is what survives,
   because a card that can't be clicked isn't worth the pixels it kept. */
@media (max-height: 700px) {
  .shell-upsell-note { display: none; }
}
@media (max-height: 640px) {
  .shell-upsell-features { display: none; }
  .shell-upsell-title { font-size: var(--text-sm); margin-bottom: var(--spacing-xs); }
}
@media (max-height: 560px) {
  .shell-upsell-title { display: none; }
}

/* Identity and Settings merged into one row, so Settings isn't a stray link
   floating between the meter and the profile. */
/* Given real presence: this is the person's identity and the way into Settings, and
   at 28px it read as an afterthought in a column with room to spare. */
.shell-acct {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 10px;
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  text-align: left;
  cursor: pointer;
  transition: background 0.12s ease;
}
.shell-acct:hover { background: var(--app-shell-hover); }
.shell-acct-av {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--text-secondary);
  color: #fff;
  /* 12px in a 36px circle left the initial looking lost. */
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: 1;
}
.shell-acct-who { min-width: 0; line-height: 1.25; }
.shell-acct-name {
  display: block;
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Plain quiet text, not a filled pill. The plan is context, not a call to action —
   the pill shouted louder than the person's own name sitting above it. */
.shell-acct-plan {
  display: block;
  margin-top: 1px;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-tertiary);
}
/* A real target, not decoration: it opens Settings directly. */
.shell-acct-gear {
  margin-left: auto;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-tertiary);
  transition: background 0.15s ease, color 0.15s ease;
}
.shell-acct-gear:hover {
  background: var(--app-shell-hover);
  color: var(--text-primary);
}

/* ---------------------------------------------------------------- canvas --- */
/* The marketing body paints seven gradient layers — three repeating-linear-gradients
   at 0/60/120deg (the triangular weave), three radial washes and a vertical fade.
   That is atmosphere for a landing page; on a surface people work in eight times a
   day it competes with the project thumbnails, which are the actual content. Flat
   neutral here, and the thumbnails supply the colour. */
/* :not(.editor-mode) is load-bearing — es-shell-flat stays on <body> when an upload
   opens the editor, so without it the editor would inherit this canvas and lose its
   footer. The editor must look exactly as it does today. */
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) {
  background-image: none;
  background-color: var(--app-shell-bg);
  /* The frame never scrolls; only the panel inside it does. */
  overflow: hidden;
}
/* scrollbar-gutter: stable (set globally in index.html) reserves ~15px at the document
   edge to stop the page jumping when a scrollbar appears. The document no longer
   scrolls here, so that reservation only makes the right gutter twice the width of the
   other three. Release it so all four match. */
html.es-shell-v2:has(body.es-shell-flat:not(.editor-mode)) { scrollbar-gutter: auto; }

/* The content panel: inset from the frame on all four sides, with its own scroll.
   Pinning it is what separates chrome from content — the rail, the gutters and the
   rounded edge stay put while the projects scroll underneath, which is the whole
   reason this pattern reads as an application rather than a long page. */
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container {
  position: fixed;
  top: var(--app-shell-gutter);
  right: var(--app-shell-gutter);
  /* Bled to the bottom edge on purpose. This region scrolls, and a sealed bottom
     with a gutter under it reads as "the content ends here" — false with 34 projects
     below the fold. Top and sides frame it; the bottom stays open because the list
     isn't finished. The 40px inset inside still keeps the last row off the edge. */
  bottom: 0;
  left: calc(var(--sidebar-width) + var(--app-shell-gutter));
  margin: 0;
  padding: 0;
  max-width: none;
  overflow-y: auto;
  /* The panel is the scroll container and .container inside it is margin-auto centred,
     so a scrollbar that comes and goes moves the whole column sideways by half its
     width. Switching a brand between settings and projects, or moving to a brand with
     no projects, changes the content height across the scroll threshold — and the brand
     title and logo visibly slide. both-edges rather than plain stable: reserving on one
     side alone stops the jump but leaves the column permanently off-centre. */
  scrollbar-gutter: stable both-edges;
  overscroll-behavior: contain;
  background: var(--app-shell-panel-bg);
  border: 1px solid var(--border);
  border-bottom: 0;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  /* A scrollbar, but the app's own. It was hidden to stop a native track clipping the
     rounded top-right corner — a fix that cost more than it saved: nothing else on the
     page says this region scrolls, Windows shows tracks permanently so their absence
     reads as broken, and anyone who drags a scrollbar rather than wheeling had no way
     down at all. A transparent track with an inset pill keeps the corner clean. */
  scrollbar-width: thin;
  scrollbar-color: rgba(15, 23, 42, 0.18) transparent;
}
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container::-webkit-scrollbar {
  width: 11px;
}
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container::-webkit-scrollbar-track {
  background: transparent;
}
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container::-webkit-scrollbar-thumb {
  /* 3px of transparent border clipped away leaves a 5px pill floating clear of the
     panel edge, rather than a bar welded to it. */
  background: rgba(15, 23, 42, 0.16);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container::-webkit-scrollbar-thumb:hover {
  background: rgba(15, 23, 42, 0.3);
  background-clip: content-box;
}
/* Content keeps its own measure inside the full-width panel, with an inset that
   matches on all four sides. Before the panel existed, .container's 24px side gutter
   sat against the browser edge and the page header carried a large top padding to
   separate it from the header bar. Inside a panel both jobs are already done by the
   panel edge, and the leftover values read as 80px above the greeting against 24px
   beside it. One inset, applied evenly. */
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container .container {
  max-width: var(--app-shell-max);
  /* All four sides come from one token. .container's own 32px vertical padding has to
     be zeroed explicitly: left over, it stacked under .shell-pagehead's inset and put
     72px above the greeting against 41px beside it — the asymmetry that reads as
     "too much space at the top". */
  padding: 0 var(--app-shell-inset-x);
}
/* Lifted slightly off the panel's top edge, and given a clearer gap below — the
   greeting and the "Recent projects" heading were reading as one block. */
/* Tightened now the box sits directly beneath: 44/48 left the greeting, the subtitle
   and the box reading as three separate things. 34/28 groups them as one header block
   without crowding — and the whole page starts a little higher. */
html.es-shell-v2 .es-shell-flat .shell-pagehead {
  padding-top: 34px;
  padding-bottom: 28px;
}
/* Breathing room at the end of the scroll, matching the inset. */
html.es-shell-v2 .es-shell-flat .bottom,
html.es-shell-v2 .es-shell-flat .workspace-section {
  padding-bottom: var(--app-shell-inset);
}
/* Card borders stay as authored: white cards sit on the tinted panel, so the surface
   contrast defines them and the hairline does not have to be pushed darker. */

/* No marketing footer on an app surface. The links it carried live on: Privacy and
   How to Use are already in the Settings sidebar, and support is reachable through
   Send Feedback in the account menu. Terms has no other home yet — it should join
   them when Settings becomes a real surface in Phase 4. */
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .site-footer {
  display: none;
}

/* ------------------------------------------- relocated header controls --- */
/* #user-profile-menu is moved into the rail foot at boot rather than reimplemented,
   so Settings, Send Feedback, Team Dashboard, pending invites, the plan badge and
   Log Out all keep working exactly as they do today. */
#app-sidebar #user-profile-menu {
  position: relative;
  width: 100%;
}
/* Our own row is the visible trigger; the original avatar button stays in the DOM
   (its listeners are what open the menu) but is not shown twice. */
#app-sidebar .profile-avatar-wrap { display: none; }

#app-sidebar #profile-dropdown-content {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  right: auto;
  top: auto;
  width: calc(var(--sidebar-width) - 24px);
  max-height: 70vh;
  overflow-y: auto;
  z-index: 130;
}
/* The usage meter is permanently visible in the rail directly below this menu —
   repeating it here is the same number twice on one screen. */
#app-sidebar #profile-dropdown-content .profile-dropdown-stats { display: none; }
/* Team belongs on the sidebar's own Team row now, not buried in the account menu. */
#app-sidebar #profile-dropdown-content #team-dashboard-link { display: none !important; }

/* Every row in the menu gets the same hover the brand rows have — without it there
   is no feedback that these are targets at all. */
#app-sidebar #profile-dropdown-content .dropdown-item {
  border-radius: var(--radius-md);
  transition: background 0.12s ease, color 0.12s ease;
}
#app-sidebar #profile-dropdown-content .dropdown-item:hover {
  background: var(--app-shell-hover);
  color: var(--text-primary);
}
#app-sidebar #profile-dropdown-content .dropdown-item:hover svg { color: var(--text-secondary); }

/* ------------------------------------------------------- workspace surface --- */
html.es-shell-v2 .shell-surface.hidden { display: none; }

/* Centred, like Canva's. The rail being left-aligned doesn't oblige the content to be:
   the panel is its own field, and the greeting is the one thing on it that addresses
   the person rather than listing their work. Section headings below stay left, so the
   grid still reads as a list rather than a poster. */
.shell-pagehead {
  min-height: var(--ws-page-header-min-height);
  padding: 48px 0 32px;
  text-align: center;
}
.shell-pagehead .shell-sub { margin-left: auto; margin-right: auto; }

/* Top inset above the greeting. It once held the upgrade pill; that moved to the rail
   foot, and the row stayed because it is what sets the panel's top inset (and, on
   narrow widths below, the clearance under the floating nav toggle). Kept empty and
   open rather than deleted: its height is an author-time constant either way, so the
   page never moves between the boot skeleton and the real greeting. */
.shell-pagehead-top {
  display: flex;
  justify-content: flex-end;
  min-height: 0;
  padding-top: var(--app-shell-inset);
  /* Anchor for the pill, which is taken out of flow below. */
  position: relative;
}
/* The slot always contains the pill element, so the old `:not(:empty)` rule here would
   have tightened the greeting on every page whether or not anything was rendered. The
   pill is positioned instead, so the row's height is a constant again and this rule
   has nothing left to do. */

/* --- What's New: a temporary tenant of the slot ----------------------------------- */
/* Quiet by construction. It is competing with a centred greeting for the top of the
   page, and it is a notice, not an action — so it gets a plain surface, a hairline and
   one small dot of colour, not a filled button. The dot is the only thing here that
   says "unread", and it leaves with the pill. */
/* Off unless index.php says otherwise. The class is the single switch: the server sets
   it, and whats-new.js clears it the moment the notice is read or dismissed — so there
   is exactly one thing that decides whether this exists on the page. */
.shell-whatsnew { display: none; }
html.es-whatsnew .shell-whatsnew {
  /* Out of flow, hard into the panel's top-right corner. In flow it sat a full inset
     down and an inset in from the edge — floating in the middle of the header rather
     than occupying the corner, and taking the greeting's spacing with it whenever it
     appeared or left. Positioned, the row's height is constant and the corner is the
     corner. The negative right pulls it out over the panel's own 60px gutter without
     reaching the edge. */
  position: absolute;
  top: 10px;
  right: -44px;
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  /* Taller than a chip: at 33px it read as a stray tag rather than something to press. */
  min-height: 40px;
  padding: 9px 16px 9px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-md);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.shell-whatsnew:hover {
  transform: translateY(-1px);
  border-color: rgba(var(--primary-rgb), 0.28);
  box-shadow: var(--shadow-md);
}
.shell-whatsnew:active { transform: translateY(0); box-shadow: var(--shadow-sm); }
.shell-whatsnew:focus-visible {
  outline: none;
  border-color: rgba(var(--primary-rgb), 0.35);
  box-shadow: 0 0 0 4px rgba(var(--primary-rgb), 0.14), var(--shadow-sm);
}
.shell-whatsnew-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--primary);
}

/* --- read, but still there ---------------------------------------------------------
   Opening the panel mutes the pill rather than removing it: the panel ends in a call
   to action that navigates away mid-read, and taking the pill at that moment strands
   whatever the reader had not reached. The dot goes, the label stops claiming news,
   and the whole thing steps back a tone. The expiry date is what finally clears it. */
.shell-whatsnew-label--read { display: none; }
html.es-whatsnew-read .shell-whatsnew-dot { display: none; }
html.es-whatsnew-read .shell-whatsnew-label { display: none; }
html.es-whatsnew-read .shell-whatsnew-label--read { display: inline; }
html.es-whatsnew-read .shell-whatsnew {
  background: transparent;
  border-color: var(--border);
  box-shadow: none;
}
html.es-whatsnew-read .shell-whatsnew .shell-whatsnew-label--read { color: var(--text-tertiary); }
html.es-whatsnew-read .shell-whatsnew:hover {
  background: var(--surface-white);
  border-color: rgba(var(--primary-rgb), 0.28);
  box-shadow: var(--shadow-sm);
}
.shell-whatsnew-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
/* The subtitle is what makes this worth opening — "What's new" alone could be anything.
   It goes first when the column narrows, since the label plus the dot still reads. */
.shell-whatsnew-sub {
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  padding-left: var(--spacing-xs);
  border-left: 1px solid var(--border);
}

/* --- What's New panel ------------------------------------------------------------- */
/* Sits on the shared .es-overlay / .es-modal-panel shell, so scrim, sizing, scroll and
   the close button behave like every other modal in the app. */
.whatsnew-panel { position: relative; }
.whatsnew-eyebrow {
  margin: 0 0 var(--spacing-xs);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--primary);
}
/* --- the two schematics --- */
/* Side by side, because the whole point is that these are the two pages that changed
   and they changed together. They sit in tinted frames so they read as pictures of
   something rather than as more interface inside the modal. */
.whatsnew-shots {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-sm);
  margin-top: var(--spacing-lg);
}
.whatsnew-shot { margin: 0; }
.whatsnew-wire {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  background: var(--app-shell-panel-bg);
}
.whatsnew-shot-cap {
  margin-top: 6px;
  text-align: center;
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

/* The schematic's own parts. Named for what they represent so the markup above stays
   readable — a wall of <rect fill="#eee"> is unmaintainable six weeks from now. */
.wire-bg { fill: var(--surface-white); stroke: var(--border); stroke-width: 1; }
.wire-rail { fill: var(--surface-wash); stroke: none; }
.wire-bar { fill: var(--surface-wash); stroke: none; }
.wire-row { fill: var(--border-strong); }
.wire-row--faint { fill: var(--border); }
.wire-accent { fill: var(--primary); }
.wire-card { fill: var(--surface-card); stroke: var(--border); stroke-width: 1; }
.wire-canvas { fill: var(--surface-card); stroke: var(--border); stroke-width: 1; }
.wire-drop {
  fill: none;
  stroke: rgba(var(--primary-rgb), 0.45);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
}
.wire-cut { stroke: rgba(var(--primary-rgb), 0.5); stroke-width: 1; stroke-dasharray: 4 3; }

/* --- the four points --- */
/* One line each, an icon to anchor it, two columns. Four paragraphs stacked was an
   essay nobody finishes; this is a glance. */
.whatsnew-points {
  list-style: none;
  margin: var(--spacing-lg) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-sm) var(--spacing-md);
}
.whatsnew-point {
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-xs);
}
.whatsnew-point-icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: rgba(var(--primary-rgb), 0.08);
  color: var(--primary);
}
.whatsnew-point-icon svg { width: 14px; height: 14px; }
.whatsnew-point-text {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text-tertiary);
}
/* The label and its line share one text block, so the line wraps under the label
   rather than being a second column that has to be kept in step with it. */
.whatsnew-point-text b {
  display: block;
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
.whatsnew-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--spacing-lg);
}

/* --- the slideshow ---------------------------------------------------------------- */
/* .es-modal-panel sets no max-width of its own, so without this the notice ran the
   full width of the viewport and every line became a 1,200px measure. */
.whatsnew-panel { max-width: 520px; }
/* One card visible at a time. The slides share a min-height so the panel does not
   jump as you move between a long card and a short one - a modal that resizes under
   the cursor moves the arrow out from under it. Sized to the tallest card, measured,
   so the arrows and dots sit on the same line throughout. */
.whatsnew-slides { min-height: 492px; }
.whatsnew-slide[hidden] { display: none; }
/* The picture leads, so it sits above the words and takes the top of the card. */
.whatsnew-shots { margin: 0 0 var(--spacing-md); }
.whatsnew-shots--one { grid-template-columns: 1fr; }
.whatsnew-title {
  margin: 0 0 var(--spacing-xs);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}
.whatsnew-lede {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-secondary);
}
/* The caveat that has to travel with the footer feature. Quiet, but set apart so it
   is not mistaken for more of the sales pitch. */
.whatsnew-note {
  margin: var(--spacing-md) 0 0;
  padding-top: var(--spacing-sm);
  border-top: 1px solid var(--border);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-tertiary);
}
.whatsnew-note b { font-weight: var(--weight-semibold); color: var(--text-secondary); }
/* A list of short lines reads better in one column than two. */
.whatsnew-points--single { grid-template-columns: 1fr; }
.whatsnew-cta { margin-top: var(--spacing-md); }
.whatsnew-cta .btn { width: 100%; }

/* A fixed three-column grid, NOT flex: the arrows occupy identical tracks whether or
   not they are usable, so the dots sit on the panel's centre line and stay there. A
   flex row with a hidden button pushed them sideways on every slide change. */
.whatsnew-nav {
  display: grid;
  grid-template-columns: 36px 1fr 36px;
  align-items: center;
  margin-top: var(--spacing-md);
  padding-top: var(--spacing-md);
  border-top: 1px solid var(--border);
}
.whatsnew-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface-white);
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.whatsnew-arrow svg { width: 17px; height: 17px; }
.whatsnew-arrow:hover:not(:disabled) {
  border-color: var(--primary);
  color: var(--primary);
  background: rgba(var(--primary-rgb), 0.06);
}
.whatsnew-arrow:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.16);
}
/* Dimmed rather than removed at the ends, so the row never changes shape. */
.whatsnew-arrow:disabled { opacity: 0.32; cursor: default; }
.whatsnew-dots {
  display: flex;
  justify-content: center;
  gap: 6px;
}
.whatsnew-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--border-strong);
  transition: background 0.15s ease, transform 0.15s ease;
}
.whatsnew-dot.is-on { background: var(--primary); transform: scale(1.25); }

/* One column for both grids on a phone: at 345px the schematics lose the detail that
   makes them worth showing, and a two-column point list becomes four words per line. */
@media (max-width: 560px) {
  .whatsnew-shots,
  .whatsnew-points { grid-template-columns: 1fr; }
}
/* Same clamp the logged-in hero used (styles/workspace.css `body.logged-in .hero-title`).
   The app-grammar mockup shrank this to 24px, but the greeting is the most recognisable
   thing on the page — dropping it 44% is what made the page read as "changed" rather
   than "improved". Familiarity wins here. */
.shell-greet {
  /* Nudged down from clamp(2rem, 3vw, 2.75rem): still the page's clear focal point
     and still close to the hero size users know, just not shouting inside a panel. */
  margin: 0;
  font-size: clamp(1.75rem, 2.6vw, 2.375rem);
  line-height: 1.08;
  font-weight: var(--weight-bold);
  letter-spacing: -0.015em;
  color: var(--text-primary);
}
.shell-greet .hero-title-accent { color: var(--primary-text); }
/* --text-lg matches the logged-in hero subtitle it replaces. */
.shell-sub {
  margin: 9px 0 0;
  font-size: var(--text-lg);
  line-height: 1.6;
  color: var(--text-tertiary);
}

/* The primary action of the whole app, so it is sized like one: 193 of the 281 real
   workflows start with a new upload against 88 resumes, roughly 2:1.
   645px is deliberate, not "about three quarters" — it is three card columns plus two
   gaps, so its edges land on the seams between the cards below rather than floating at
   an arbitrary width. Centred on the same axis as the greeting above it.
   Dashed because it is a drop target, not a content card. */
.shell-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: 100%;
  max-width: 645px;
  margin: 0 auto 44px;
  padding: 30px 24px;
  border: 1.5px dashed rgba(var(--primary-rgb), 0.34);
  border-radius: var(--radius-xl);
  background: var(--surface-white);
  box-shadow: 0 4px 18px rgba(var(--primary-rgb), 0.05);
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.shell-drop:hover,
.shell-drop.drag-over {
  border-color: var(--primary);
  background: var(--surface-white);
  box-shadow: 0 10px 30px rgba(var(--primary-rgb), 0.12);
}
.shell-drop:focus-visible {
  outline: none;
  box-shadow: 0 0 0 4px rgba(var(--primary-rgb), 0.18);
}
.shell-drop-icon {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 14px;
  background: var(--primary-subtle);
  color: var(--primary);
}
.shell-drop-icon svg { width: 26px; height: 26px; }
.shell-drop-title {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  color: var(--text-primary);
}
.shell-drop-sub {
  margin-top: 4px;
  font-size: var(--text-md);
  color: var(--text-secondary);
}

/* ------------------------------------------------------------ brand page --- */
html.es-shell-v2 .shell-pagehead.hidden { display: none; }

/* The brand header stays left: it's a label for the content beneath it, not a greeting. */
.shell-brandhead { text-align: left; }
.shell-brandhead-row { display: flex; align-items: flex-start; gap: 15px; }
/* Takes the slack so the settings control lands on the right edge, not beside the name. */
.shell-brandhead-text { flex: 1 1 auto; min-width: 0; }

.shell-mark-wrap { position: relative; flex-shrink: 0; margin-top: 2px; }
.shell-brand-mark-lg {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: var(--weight-extrabold);
  font-size: var(--text-lg);
  background: var(--text-secondary);
}
.shell-mark-edit {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-lg);
  background: rgba(15, 23, 42, 0.55);
  color: #fff;
  opacity: 0;
  cursor: pointer;
  transition: opacity 0.15s ease;
}
.shell-mark-wrap:hover .shell-mark-edit,
.shell-mark-edit:focus-visible { opacity: 1; }

.shell-swatches {
  position: absolute;
  top: 52px;
  left: 0;
  z-index: 30;
  width: 196px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-white);
  box-shadow: var(--shadow-xl);
}
.shell-swatches.hidden { display: none; }
.shell-swatch-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 7px; }
.shell-swatch {
  width: 22px;
  height: 22px;
  border-radius: var(--radius-sm);
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
}
.shell-swatch:hover { transform: scale(1.12); }
.shell-swatch.is-active { border-color: var(--text-primary); }

.shell-brandname-row { display: flex; align-items: center; gap: var(--spacing-xs); }
.shell-brandname {
  margin: 0;
  font-size: clamp(1.75rem, 2.6vw, 2.375rem);
  line-height: 1.08;
  font-weight: var(--weight-bold);
  letter-spacing: -0.015em;
  color: var(--text-primary);
}
.shell-rename {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, background 0.15s ease;
}
.shell-brandhead:hover .shell-rename,
.shell-rename:focus-visible { opacity: 1; }
.shell-rename:hover { background: var(--app-shell-hover); color: var(--text-secondary); }

.shell-brandstatus {
  margin: 9px 0 0;
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  font-size: var(--text-sm);
  color: var(--text-tertiary);
}
.shell-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}
.shell-tag.is-ok {
  color: var(--success);
  border-color: var(--success-border);
  background: var(--success-subtle);
}
.shell-tag.is-missing {
  color: var(--warning-amber);
  border-color: var(--warning-border);
  background: var(--warning-subtle);
}

/* Brand form host. The relocated .kbm-form carries the modal's inline styles, so the
   overrides here are what make it read as a page section rather than a dialog body. */
.shell-brand-setup {
  margin-bottom: var(--app-shell-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-white);
  box-shadow: var(--shadow-sm);
}
.shell-brand-setup.hidden { display: none; }
.shell-setup-name-host.hidden { display: none; }

/* --- Settings rows -------------------------------------------------------------
   The page-native shape, replacing the modal form that was dropped here wholesale:
   the setting's name and a real explanation on the left, its control on the right.
   Full width, matching the section heading and card grid it sits above. Narrowing it
   was the fix for the old dead right-hand column — but that column existed because a
   single stacked form was stretched across it. With two columns the width is used, so
   the card has no reason to be narrower than everything else on the page. */
.shell-set-row {
  display: grid;
  /* The copy column drives the height of every row whose control is a single input, so
     width spent here is height saved. The inputs do not need 500px — a URL reads fine
     in 420 — and moving that space to the left turns four-line explanations into two.
     Widening the card made it shorter, which is the opposite of what it looks like it
     should do. */
  grid-template-columns: 340px minmax(0, 1fr);
  /* Column gap only. `gap` also sets the row gap, and the help pane is a third grid
     item on its own line — so 40px of row gap stacked on top of the pane's own margin
     and pushed it 56px away from the field it belongs to. */
  column-gap: 40px;
  row-gap: 0;
  padding: 26px 0;
}
.shell-set-row + .shell-set-row { border-top: 1px solid var(--border); }
.shell-set-label {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
.shell-set-help {
  margin: 5px 0 0;
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  line-height: 1.5;
  /* Even out the wrap so the last line is never a single orphaned word. */
  text-wrap: pretty;
}
/* The row supplies the label, so the field group's own is suppressed rather than shown
   twice. The API-key hint stays where it was authored — directly under its input, with
   "How to get your API key" opposite it — because it describes what happens when you
   press Save, which belongs beside the control, not in the column explaining what the
   setting is for. */
.shell-set-field [data-brand-field] > label { display: none !important; }

/* Klaviyo blocks editor. Shared between the modal and the page slot; only
   sizing/spacing here, state (hidden per keyed/unkeyed brand) is class-driven. */
.brand-footer-uc-picker { position: relative; margin-top: 4px; }
.bfu-blocks-explainer {
  margin: 0 0 8px;
  font-size: var(--text-sm);
  color: var(--text-tertiary);
}
/* The page's left column already explains the setting, so the inline explainer is
   the modal's alone. */
.shell-set-field .bfu-blocks-explainer { display: none; }
.bfu-blocks-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 8px;
}
.bfu-blocks-list.hidden { display: none; }
.bfu-blocks-list.is-drag-active { user-select: none; }
.bfu-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-white);
}
.bfu-row.is-dragging {
  border-color: var(--primary);
  box-shadow: var(--shadow-md);
  position: relative;
  z-index: 5;
}
.bfu-row-handle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 26px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-tertiary);
  cursor: grab;
  touch-action: none;
  flex-shrink: 0;
}
.bfu-row-handle:hover:not(:disabled) { color: var(--text-secondary); background: var(--surface-canvas); }
.bfu-row-handle:disabled { opacity: 0.35; cursor: default; }
.bfu-row.is-dragging .bfu-row-handle { cursor: grabbing; }
.bfu-row-order {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--text-tertiary);
  min-width: 12px;
  text-align: center;
  flex-shrink: 0;
}
.bfu-row-thumb {
  width: 34px;
  height: 22px;
  object-fit: cover;
  object-position: top;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-canvas);
  flex-shrink: 0;
}
.bfu-row-thumb.hidden { display: none; }
.bfu-row-name {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 0;
  border: none;
  background: transparent;
  font-family: var(--font-sans);
  cursor: pointer;
  text-align: left;
}
.bfu-row-name-text {
  max-width: 100%;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bfu-row-name:hover .bfu-row-name-text { color: var(--primary); }
.bfu-row-missing-note {
  font-size: var(--text-xs);
  color: var(--warning-amber);
}
.bfu-row-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  cursor: pointer;
  flex-shrink: 0;
  white-space: nowrap;
}
.bfu-row-toggle input { accent-color: var(--primary); margin: 0; }
.bfu-row-remove {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-tertiary);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}
.bfu-row-remove:hover { color: var(--danger); background: var(--surface-canvas); }
.bfu-add-btn {
  display: inline-flex;
  align-items: center;
  padding: 7px 12px;
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-white);
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.bfu-add-btn:hover { border-color: var(--primary-muted); color: var(--text-primary); }
.bfu-add-btn[aria-expanded="true"] { border-color: var(--primary); color: var(--text-primary); }
.bfu-add-btn.hidden { display: none; }
.brand-footer-uc-panel {
  position: absolute;
  /* Opens upward: the row sits low on the brand page (below the API key), so the
     space is above it - dropping down clipped the grid almost immediately. */
  bottom: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 40;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-white);
  box-shadow: var(--shadow-lg);
}
.bfu-search {
  width: 100%;
  padding: 7px 10px;
  margin-bottom: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  color: var(--text-primary);
  background: var(--surface-canvas);
  box-sizing: border-box;
}
.bfu-search:focus { outline: none; border-color: var(--primary); background: var(--surface-white); }
.bfu-grid {
  display: grid;
  /* Three-up with small thumbs: a footer is recognisable at stamp size, and the
     extra column keeps far more of the library on screen at once.
     minmax(0, 1fr), NOT 1fr: a bare 1fr floors each track at its content's
     min-width, and the card names are nowrap — so one long block name widened its
     column, threw the other two out of proportion and forced the whole panel to
     scroll sideways. Zero floor lets the ellipsis do its job instead. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  /* The panel opens upward into most of the viewport, so it can afford to be tall.
     Capped against the viewport too, for short screens where the row sits high. */
  max-height: min(440px, 52vh);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
}
.bfu-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;   /* same reason: a flex item's auto minimum would re-introduce it */
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-white);
  font-family: var(--font-sans);
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease;
}
.bfu-card:hover:not(:disabled) { border-color: var(--primary-muted); }
.bfu-card.is-selected { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
/* Already one of the brand's blocks: visible in the library but not pickable twice. */
.bfu-card.is-added { opacity: 0.55; cursor: default; }
.bfu-card-added {
  margin-left: 6px;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.bfu-card-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  /* Footer blocks are the tall ones, and 52px of a 600x900 design showed almost
     nothing. Roughly square at this column width, which is about two thirds of a
     typical footer instead of a sliver - still cropped from the top, because the
     top is the recognisable part. */
  height: 118px;
  object-fit: cover;
  object-position: top;
  border-radius: var(--radius-sm);
  background: var(--surface-canvas);
  border: 1px solid var(--border);
  box-sizing: border-box;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}
.bfu-card-name {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bfu-empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: 14px 8px;
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  text-align: center;
}
.bfu-empty-detail { font-size: var(--text-xs); color: var(--text-tertiary); }
.bfu-retry {
  grid-column: 1 / -1;
  justify-self: center;
  margin-bottom: 6px;
  padding: 6px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-white);
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
  cursor: pointer;
}
.bfu-retry:hover { border-color: var(--primary-muted); color: var(--text-primary); }
/* Placeholders while a block's record is in flight. Without them the thumbnail was
   simply absent for a second or two and then appeared, which read as a glitch and
   shunted the rows below it down as it arrived. */
@keyframes bfuShimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.bfu-row-thumb.is-loading {
  background-image: linear-gradient(90deg,
    var(--surface-canvas) 25%, var(--surface-wash) 37%, var(--surface-canvas) 63%);
  background-size: 200% 100%;
  animation: bfuShimmer 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .bfu-row-thumb.is-loading { animation: none; }
}

.brand-footer-uc-picker.hidden,
.brand-footer-uc-panel.hidden { display: none; }
/* No API key means no blocks to choose, so the setting is withheld entirely — the
   field group in the modal, and on the brand page the row that titles it. */
[data-brand-field="footerblock"].hidden,
.shell-set-row.hidden { display: none !important; }
/* The ★ explanation is in the row's copy on the left, so the version the link editor
   shows under the fields would be saying it twice. The API-key hint below is a
   different case and stays: it describes what happens on save, not what the setting is.
   (This was hidden, then lost when the rule above was rewritten to restore that hint.) */
.shell-set-field #klaviyo-brand-links-hint { display: none !important; }
.shell-set-field > [data-brand-field] { margin-bottom: 0 !important; }

/* The help pane, rebuilt as a card. In the modal it is a panel docked to the right edge
   — hence `flex: 0 0 46%`, a lone border-left, right-hand corners only, and negative
   margins to bleed off three sides. Dropped underneath the field on the page, all of
   that read as a panel sliced off at the top and bottom. Here it is a self-contained
   card spanning both columns, so the explanation on the left stays visible. */
.shell-set-row > #klaviyo-key-help {
  grid-column: 1 / -1;
  flex: none;
  /* Close enough to read as belonging to the field above it, not as a separate block. */
  margin: 12px 0 2px;
  padding: var(--spacing-lg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-canvas);
}
/* Its head was sized for a narrow dock; at full width it can carry a page-section
   title, with more air beneath it before the steps begin. */
.shell-set-row > #klaviyo-key-help .kbm-help-head { margin-bottom: var(--spacing-md); }
.shell-set-row > #klaviyo-key-help .kbm-help-title { font-size: var(--text-lg); }

/* One disclosure, summarising its own values so the header above needn't repeat them.
   No top border: it is now the card's only child, so a rule there drew a line under
   nothing. */
.shell-srow { border-top: 0; }
/* A real control on the brand's own header row, held to the right edge of the measure
   and sized to match the name opposite it. Two lines — what it is, and what it is
   currently set to — so it carries information rather than just being a lid. */
.shell-srow-head {
  flex: 0 0 auto;
  margin-left: auto;
  align-self: center;
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  /* Sized to its contents. A fixed 272px left 126px of empty white beside 146px of
     text, which is what made it read as a slab rather than a control. */
  max-width: 340px;
  padding: 11px 14px 11px 16px;
  border: 1px solid var(--border);
  /* --radius-lg, not --radius-md. The corner ladder in this file is frame 18 → block 12
     → control 10, and at 61px tall carrying two lines this is a block, not a control.
     Everything else in the app of that size — project cards, the usage meter — is 12.
     It was wearing a 38px button's radius at 61px, which is the "feels different". */
  border-radius: var(--radius-lg);
  background: var(--surface-white);
  cursor: pointer;
  text-align: left;
  transition: background 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease;
}
/* Stated locally: the display:flex above is authored after the global
   `.hidden { display: none }` at equal specificity, so the shorthand alone loses.
   /brand/new hides this control — there are no projects to go back to yet. */
.shell-srow-head.hidden { display: none; }
/* No resting shadow, matching the usage meter — its real sibling: a bordered block
   holding a label and its current value. The border alone separates it from the panel;
   the lift arrives on hover to say it is pressable. */
.shell-srow-head:hover {
  border-color: rgba(var(--primary-rgb), 0.42);
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.05);
}
/* Open, it reads as the head of the panel below it rather than a raised button. */
.shell-srow-head[aria-expanded="true"] {
  background: var(--surface-canvas);
  border-color: rgba(var(--primary-rgb), 0.42);
  box-shadow: none;
}
.shell-srow-head:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.35);
}
.shell-srow-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.shell-srow-cv {
  color: var(--text-tertiary);
  flex-shrink: 0;
  margin-left: auto;
  transition: transform 0.18s ease;
}
/* Keyed off the button's own state: the caret no longer lives inside .shell-srow.
   Points right when closed, down when open — the panel drops below it. */
.shell-srow-head[aria-expanded="true"] .shell-srow-cv { transform: rotate(90deg); }
.shell-srow-label {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  white-space: nowrap;
}
/* The summary stays visible whether open or closed. Hiding it on open would change
   the control's height and make the header jump; left in place it doubles as a live
   preview of what the panel below is set to. */
.shell-srow-val {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Closed, the card is not in the flow at all — that is the whole point of moving the
   toggle up. Open, it drops in below the header where the old card used to sit. */
html.es-shell-v2 .shell-brand-setup:not(.is-open) { display: none; }
/* Symmetric padding now the body holds full-width rows rather than a form indented
   under the disclosure label. */
.shell-srow-body { display: none; padding: 26px 28px; }
.shell-srow.is-open .shell-srow-body { display: block; }
/* Delete stays reachable at the foot of the disclosure, never promoted above it.
   Deliberately NOT display:flex — the field group inside already is, with its own
   justify-content:space-between. Making this a flex container too shrank that group to
   its content (282px in an 808px row), which bunched Delete, Cancel and Save together
   on the left instead of pushing Delete away from the pair. As a block, the group
   fills the width and its own rule does the separating. */
.shell-setup-actions {
  padding-top: 22px;
  border-top: 1px solid var(--border);
}

/* Refusing to close or navigate is explained here, beside the two buttons that
   resolve it, rather than in a dialog that covers the form it is talking about. */
.brand-actions-note {
  display: none;
  flex: 1;
  min-width: 0;
  padding: 0 var(--spacing-sm);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--warning-amber);
  text-align: right;
  /* The row does not set align-items, so the default stretch made this span as tall
     as the buttons and left its text sitting at the top of that box. */
  align-self: center;
}
.brand-actions-note.is-visible { display: block; }
@media (max-width: 560px) {
  .brand-actions-note { flex-basis: 100%; text-align: left; padding: 0 0 var(--spacing-xs); }
}
/* One short pulse when the refusal happens, so the eye is taken to the buttons
   even if the note was already on screen from a previous attempt. */
@keyframes brandActionsFlag {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-2px); }
  80% { transform: translateX(2px); }
}
[data-brand-field="actions"].is-flagging { animation: brandActionsFlag 0.45s ease; }
@media (prefers-reduced-motion: reduce) {
  [data-brand-field="actions"].is-flagging { animation: none; }
}
@media (max-width: 780px) {
  /* Stack the label above its control rather than squeezing both into a narrow row. */
  .shell-set-row { grid-template-columns: 1fr; gap: 10px; }
}

/* The heading belongs to the grid beneath it, but they live in two sections, each
   inside its own .container (32px vertical padding) plus .workspace-section's own
   padding. Three paddings stacked to ~104px of dead air. Collapse them all and let
   this one margin be the only thing setting the distance. */
/* The box is permanent, for everyone. It was briefly hidden for users who had projects,
   on the reasoning that only 13% of uploads were drags so a standing drop target served
   a minority. That reasoning was wrong: campaign_workflows.upload_method records the
   MECHANISM (a picker opened vs a file dragged), not which affordance was clicked — and
   the old UI had three click targets all logging file_input. The data ruled out the box
   as a drag target, which the whole-panel drop zone now covers; it said nothing about
   the box as a click target, which is what it mostly was. */

/* The whole panel accepts the drop (see bindShellPanelDrop), but only the box lights up.
   Outlining the entire panel read as the whole screen being selected; the box is where
   the file is going, so it is the thing that should react. */
html.es-shell-v2 .es-shell-flat .app-main-container.is-drag-over .shell-drop {
  border-color: var(--primary);
  border-style: solid;
  background: var(--primary-subtle);
  box-shadow: 0 10px 30px rgba(var(--primary-rgb), 0.12);
}

.shell-sectionhead { margin-bottom: var(--spacing-lg); }
.shell-sectionhead h2 {
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
html.es-shell-v2 .es-shell-flat .shell-surface .container { padding-bottom: 0; }
/* Only the TOP is collapsed here. This rule used to zero the bottom too, which quietly
   overrode the breathing room set further up the file — same specificity, later in the
   source — and left the retention note sitting flat on the bottom edge of the panel.
   The panel is deliberately bled to the viewport bottom, so nothing else was going to
   provide that space. */
html.es-shell-v2 .es-shell-flat .workspace-section { padding-top: 0; }
html.es-shell-v2 .es-shell-flat .workspace-section .container { padding-top: 0; }

/* --------------------------------------- flatten the legacy workspace card --- */
/* The grid and its render pipeline are reused as-is; only the card chrome around
   them goes, so the projects sit on the page instead of inside a raised panel. */
html.es-shell-v2 .es-shell-flat .workspace-container {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  overflow: visible;
}
html.es-shell-v2 .es-shell-flat .workspace-container::after { display: none; }
html.es-shell-v2 .es-shell-flat .workspace-header { display: none; }

/* One texture on the page, not three: the hero grid and the card grid both go. */
html.es-shell-v2 .es-shell-flat .hero-glow { display: none; }

/* Corner language: radii step down as you nest inward, rather than each element
   picking its own. Frame 18 (panel, drop target) → blocks 12 (cards, meter, setup
   card) → controls 10 (buttons, rows, gear) → marks 6 (sub-20px tiles, kept
   proportional to their size). Cards were 14, between two steps and belonging to
   neither. Controls sat at 8 and read square next to everything else — 10 matches
   the app-wide .btn, so a button in the rail and a button in a modal are the same
   shape. */
html.es-shell-v2 .es-shell-flat .workspace-section .history-item {
  border-radius: var(--radius-lg);
}

/* Slightly more air than the legacy grid, matching the reviewed mockup. */
html.es-shell-v2 .es-shell-flat .recent-projects-grid {
  grid-template-columns: repeat(auto-fill, minmax(186px, 1fr));
  gap: var(--spacing-lg);
}
html.es-shell-v2 .es-shell-flat .workspace-section .history-thumb { height: 150px; }

/* --------------------------------------------------------------- responsive --- */
/* Below 980px a 280px rail would take a third of the canvas, so it goes off-canvas and
   is summoned by a toggle. The previous version of this block hid the rail and tried to
   restore the old top header — but #route-boot-styles hides that header with
   !important at higher specificity, so it never came back and a 900px window had no
   navigation whatsoever, plus 294px of dead space where the rail used to be.
   The off-canvas positions themselves live in the boot styles so they are right on the
   first paint; everything below is motion, chrome and the open state. */
.shell-nav-toggle {
  position: fixed;
  top: 20px;
  left: 20px;
  z-index: 130;
  width: 40px;
  height: 40px;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-white);
  color: var(--text-secondary);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.shell-nav-toggle:hover { background: var(--app-shell-hover); color: var(--text-primary); }
.shell-nav-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.35);
}
.shell-nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 119;
  background: rgba(15, 23, 42, 0.32);
}

@media (max-width: 980px) {
  /* Opaque and lifted once it is floating over the panel — the desktop rail is
     deliberately transparent because it sits on the page background, which is exactly
     wrong for a drawer. */
  html.es-shell-v2 #app-sidebar {
    background: var(--app-shell-bg);
    box-shadow: var(--shadow-xl);
    transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
  }
  /* The toggle sits where the logo would be, so the page header clears it. */
  html.es-shell-v2 .es-shell-flat .shell-pagehead,
  html.es-shell-v2 .es-shell-flat .shell-brandhead { padding-top: 34px; }
  html.es-shell-v2 .es-shell-flat .shell-pagehead-top { padding-top: 60px; }
}

/* Tighter frame on small screens: a 52px inset either side of a 700px window spends
   15% of the width on margin. */
@media (max-width: 760px) {
  html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container .container {
    padding: 0 24px;
  }
  html.es-shell-v2 .es-shell-flat .shell-brandhead-row { gap: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  html.es-shell-v2 #app-sidebar { transition: none; }
}

/* --- Project card: what the thumbnail actually shows ------------------------------
   An email design is a tall narrow image; the thumb window is 197x150 landscape. With
   object-position centred, `cover` throws away the top and bottom and shows a band from
   the middle of the email — usually body copy, which looks the same on every card. The
   top of the email is the logo and hero, which is what makes one recognisable at a
   glance. */
html.es-shell-v2 .es-shell-flat .workspace-section .history-thumb {
  position: relative;
}
/* :not(.history-preview) is load-bearing. This selector also matched the hover
   preview — and being one element more specific than the preview's own rule, it won,
   so the preview laid out `position: relative` in normal flow and appeared BESIDE the
   thumbnail instead of over it. */
html.es-shell-v2 .es-shell-flat .workspace-section .history-thumb img:not(.history-preview) {
  position: relative;
  z-index: 1;
  object-position: top center;
}
/* Uploads are cleaned up on a retention schedule while the campaign row stays, so an
   older project's image 404s and createHistoryCard() hides it — which left a bare grey
   rectangle with no explanation. Keyed off .is-empty, set by the same handler, rather
   than sitting under every thumbnail: a placeholder behind a transparent PNG would
   show through. */
html.es-shell-v2 .es-shell-flat .workspace-section .history-thumb.is-empty::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0.34;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 26px 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='m21 15-5-5L5 21'/%3E%3C/svg%3E");
}

/* Buttons do not inherit font by default, so any button whose type is set on its
   children was silently running at the UA's 13.333px — .shell-acct, the gear, the
   rename pencil, the mark editor, the disclosure head and the card delete button all
   were. Harmless today, but it means anything added inside them lands off-scale. */
html.es-shell-v2 button { font-family: inherit; }
html.es-shell-v2 .shell-acct,
html.es-shell-v2 .shell-acct-gear,
html.es-shell-v2 .shell-rename,
html.es-shell-v2 .shell-mark-edit,
html.es-shell-v2 .shell-srow-head,
html.es-shell-v2 .es-shell-flat .history-delete-btn {
  font-size: var(--text-base);
}

/* .hidden is a single class, and so are .shell-mark-edit / .shell-rename — and this
   file loads last, so their own display won. The create-a-brand surface hides both
   (there is no brand yet to recolour or rename), which silently did nothing. */
html.es-shell-v2 .shell-mark-edit.hidden,
html.es-shell-v2 .shell-rename.hidden,
html.es-shell-v2 .shell-drop.hidden { display: none; }

/* --- "How to get your API key", on the page ------------------------------------
   The chevron points right because in the editor the pane docks to the right of the
   form. Here it opens downwards, so the arrow has to say so: down when closed, up when
   open. Scoped to the page — the modal's rightward arrow is still correct there. */
.shell-set-field .kbm-key-help-toggle .kbm-toggle-chevron {
  transform: rotate(90deg);
  transition: transform 0.18s ease;
}
.shell-set-field .kbm-key-help-toggle[aria-expanded="true"] .kbm-toggle-chevron {
  transform: rotate(-90deg);
}

/* Scopes in two columns. Docked at 46% width the name and its access chip sat close
   together; across 760px they ended up at opposite ends of the card with a river of
   space between, and the pairing stopped reading. Two columns halve the distance and
   put the width to work, instead of capping the list and leaving the right half empty. */
.shell-set-row > #klaviyo-key-help .kbm-perm {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--spacing-md);
}
@media (min-width: 900px) {
  /* :not([hidden]) is load-bearing. Setting display on this element outranks the UA's
     [hidden] { display: none }, so without it the pane could never be closed again —
     it sat open permanently and the X did nothing. */
  .shell-set-row > #klaviyo-key-help:not([hidden]) {
    /* The four scope rows form a 2x2 block. Only they move — the paragraphs above and
       the guarantee below stay full width, so the card keeps one reading order. */
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--spacing-xl);
  }
  .shell-set-row > #klaviyo-key-help > *:not(.kbm-perm) { grid-column: 1 / -1; }
}

/* --- The brand name is edited in place ------------------------------------------
   The heading is the field. Nothing announces it until you go near it: hover shows a
   soft plate, focus shows a real one with a ring. A permanently boxed H1 would look
   like a form and cost the page its title. */
html.es-shell-v2 #shell-brandname[contenteditable] {
  outline: none;
  border-radius: var(--radius-md);
  padding: 2px 8px;
  margin-left: -8px;
  cursor: text;
  transition: background 0.12s ease, box-shadow 0.12s ease;
  min-width: 1ch;
}
html.es-shell-v2 #shell-brandname[contenteditable]:hover { background: var(--app-shell-hover); }
html.es-shell-v2 #shell-brandname[contenteditable]:focus {
  background: var(--surface-white);
  box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.45);
}
/* Empty on the create surface, so it needs to say what goes here. */
html.es-shell-v2 #shell-brandname[contenteditable]:empty::before {
  content: attr(data-placeholder);
  color: var(--text-quaternary);
  pointer-events: none;
}
/* The create surface starts focused, so it starts in its focused skin too. */
html.es-shell-v2 #shell-brandname.is-new { background: var(--surface-white); }

/* --- Time-of-day tint ------------------------------------------------------------
   A soft bloom behind the greeting whose hue follows the clock: warm amber before
   11, the app's own indigo through the working day, cool violet after 5. Chosen over
   the pattern options because it adds atmosphere without adding anything to look at
   — there is no motif to recognise, get bored of, or clash with a user's own work.

   The daypart attribute is set in index.html's boot script (before first paint) and
   refreshed by syncDaypart() in workspace.js. Purely decorative.

   The bloom fills the panel's border box and border-radius clips it — a square of
   gradient with the corners cut off. `background-attachment: local` is deliberately
   not used: local paints the scrolling content box, which sits inside the curve, so
   the radius had nothing to clip. A second wash on the frame behind the panel was
   tried so the quarter-circles would not show the white gutter; it leaked into that
   gutter and read as the bloom overhanging the corner. The gutter stays white. */
html.es-shell-v2 { --daypart-tint: 99, 102, 241; }
html.es-shell-v2[data-daypart="morning"] { --daypart-tint: 245, 158, 11; }
html.es-shell-v2[data-daypart="day"]     { --daypart-tint: 99, 102, 241; }
html.es-shell-v2[data-daypart="evening"] { --daypart-tint: 129, 90, 220; }

html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container {
  background-image: radial-gradient(110% 85% at 50% -22%,
    rgba(var(--daypart-tint), 0.13) 0%,
    rgba(var(--daypart-tint), 0.045) 45%,
    transparent 76%);
  background-repeat: no-repeat;
  background-origin: border-box;
  background-clip: border-box;
  background-size: 100% 380px;
  background-position: 0 0;
  transition: background-image 0.6s ease;
}
/* The page content has to sit above the tint, or the bloom washes over the greeting
   instead of sitting behind it. */
html.es-shell-v2 .es-shell-flat #shell-surface,
html.es-shell-v2 .es-shell-flat #workspace-section {
  position: relative;
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container {
    transition: none;
  }
}

/* --- Brand settings: a sideways swap, not a drawer -------------------------------
   Settings and projects are two different jobs, and there is no moment where you want
   both. So the projects side leaves to the left and settings arrives from the right,
   with the brand's own header staying put as the fixed point — the page reads as one
   surface moving rather than a panel unfolding.

   Deliberate tweak to "slide the whole width": the two sides travel 140px, not a full
   screen. A full traverse has to be clipped by an overflow container, which means
   restructuring the DOM so the projects grid and the settings card share a parent —
   and the grid lives in a sibling section that has to stay where it is for flag-off
   rollback. 140px with a fade reads as the same gesture, costs no structure, and is
   quicker to sit through on every visit. */
html.es-shell-v2 body.es-shell-flat:not(.editor-mode) .app-main-container { overflow-x: hidden; }

html.es-shell-v2 .shell-slide-group {
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
  will-change: transform, opacity;
}
html.es-shell-v2 .shell-slide-group.is-out {
  transform: translateX(-140px);
  opacity: 0;
  pointer-events: none;
}
/* The settings card starts off to the right and settles into place. */
html.es-shell-v2 .shell-brand-setup.is-entering {
  transform: translateX(140px);
  opacity: 0;
}
html.es-shell-v2 .shell-brand-setup.is-settled {
  transform: translateX(0);
  opacity: 1;
  transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.26s ease;
}
@media (prefers-reduced-motion: reduce) {
  html.es-shell-v2 .shell-slide-group,
  html.es-shell-v2 .shell-brand-setup.is-settled { transition: none; }
}

/* The control is a two-way door now: right means "go to settings", left means "come
   back". Replaces the downward caret, which described a drawer that no longer exists. */
html.es-shell-v2 .shell-srow-head[aria-expanded="true"] .shell-srow-cv {
  transform: rotate(180deg);
}


/* --- Hover preview -----------------------------------------------------------------
   Hold the pointer on a card and the real design loads into its thumbnail and scrolls
   itself. Nothing is fetched until intent is proven, so browsing the grid costs no more
   than it does today; the source images are large and this would be indefensible on
   plain hover. Top-cropped thumbnails make many campaigns look alike at a glance, so
   this is the fastest way to tell two similar emails apart without opening either. */
html.es-shell-v2 .es-shell-flat .workspace-section .history-thumb img.history-preview {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  /* Not object-fit:cover — the whole point is the natural height, so it can travel. */
  object-fit: fill;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
html.es-shell-v2 .es-shell-flat .workspace-section .history-thumb img.history-preview.is-in { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  /* The reveal is still useful; only the travel is motion. */
  html.es-shell-v2 .es-shell-flat .workspace-section .history-preview {
    transition: none;
    transform: none !important;
  }
}

/* ============================================================================
   EDITOR — the project page, brought into the shell.

   Gate is `html.es-shell-v2 body.editor-mode` and nothing else. Not a route class:
   after an upload started from /workspace the URL never changes (processImageFile
   only rewrites it when a project id already exists), so es-route-project is absent
   on the most common path into the editor. editor-mode is set from image state, which
   is exactly when the editor is on screen, on every path.

   Note this file's header rule about --header-height / --editor-content-offset: the
   sidebar must never READ them. Here we only redefine --editor-content-offset for the
   editor, which is the one place that is allowed to own it.
   ============================================================================ */

html.es-shell-v2 body.editor-mode {
  /* Clears the pinned bar: 14 top padding + 38 control + 10 bottom padding, plus the
     gap. The canvas is free to scroll under the bar; the sticky right panel is not, or
     the brand chip would sit behind the buttons. */
  --editor-content-offset: calc(62px + var(--editor-bar-gap));
  background-image: none;
  background-color: var(--app-shell-panel-bg);
}

/* No marketing header on a project page. */
html.es-shell-v2 body.editor-mode .app-header { display: none !important; }

/* Full bleed. .container and .app-main-container are the same element, so this one
   rule clears both the 1280px cap and the 32/24 padding. The padding moves to the two
   children so the sticky bar's opaque band can reach the window edges. */
html.es-shell-v2 body.editor-mode .app-main-container {
  max-width: none;
  padding: 0;
}
html.es-shell-v2 body.editor-mode #editor-bar,
html.es-shell-v2 body.editor-mode #editor-screen {
  padding-left: var(--editor-bleed-x);
  padding-right: var(--editor-bleed-x);
}
html.es-shell-v2 body.editor-mode #editor-screen {
  padding-bottom: var(--editor-bleed-x);
  /* editor.css sets min-height:100vh; with a bar above it that guarantees a scrollbar
     and ~56px of dead space on every short email. */
  min-height: calc(100vh - var(--editor-content-offset));
}

/* --- the bar ------------------------------------------------------------------- */
/* Default-off and unscoped, so the element is inert with the flag off. */
#editor-bar { display: none; }
html.es-shell-v2 body.editor-mode #editor-bar {
  display: flex;
  /* flex-start, set with the stacked left group below — `center` here never applied. */
  justify-content: space-between;
  gap: var(--spacing-md);
  /* Sticky, but with NO background band. Two failed attempts got us here: opaque and
     pinned, it clipped the top of the email like the header it replaced; unpinned, the
     controls scrolled away entirely. Transparent and pinned, the design passes behind
     the pills and still reaches the very top of the screen, while every control stays
     put. The pills are white with their own shadow, so they hold against artwork.
     Padding rather than a fixed height, so the extra air above lives in the element
     and --editor-content-offset can be derived from it. */
  position: sticky;
  top: 0;
  /* Above #command-bar's 100. A sticky element with a z-index creates a stacking
     context, so the brand menu's own z-index only competes INSIDE this bar — raising
     the menu alone did nothing, because the whole bar was losing to the zoom pill. */
  z-index: 110;
  background: transparent;
  padding-top: 14px;
  padding-bottom: 10px;
  pointer-events: none;   /* the gaps between controls must not eat canvas clicks */
}
html.es-shell-v2 body.editor-mode #editor-bar > * { pointer-events: auto; }
html.es-shell-v2 body.editor-mode .editor-bar-side {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  min-width: 0;
}
/* Left group stacks; right group stays on the first line beside it. */
html.es-shell-v2 body.editor-mode .editor-bar-side--start {
  position: relative;
  flex-direction: column;
  align-items: flex-start;
}
/* The brand row is taken OUT of flow. In flow it added ~48px to the bar's height, and
   everything below inherited that — including the snippets panel on the far right,
   which has no business clearing a control on the far left. Floating it means the bar
   costs one row of height while showing two, and the canvas and the panel both rise. */
html.es-shell-v2 body.editor-mode #editor-bar-brand {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 10px;
}
html.es-shell-v2 body.editor-mode .editor-bar-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  min-width: 0;
}
html.es-shell-v2 body.editor-mode #editor-bar { align-items: flex-start; }

/* The chip's menu is authored right-aligned for a panel on the right edge. On the left
   it has to open from its own left edge, or it hangs out toward the middle of the
   canvas. */
html.es-shell-v2 body.editor-mode #editor-bar .brand-export-menu {
  right: auto;
  left: 0;
}
/* Its old home is empty now — collapse it so the snippets card takes the space back. */
html.es-shell-v2 body.editor-mode .panel-header-row { display: none; }
/* Long brand names must not stretch the bar across the artwork. */
html.es-shell-v2 body.editor-mode #editor-bar .brand-dropdown-trigger { max-width: 260px; }
html.es-shell-v2 body.editor-mode #editor-bar #active-brand-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Back: the quietest thing in the bar. Weight order reads back < status < undo/redo
   < New Image, which is the order of how often each is used. */
/* A pill, not a bare label. It is the only way out of the editor, so it has to read as
   a control at rest rather than only when hovered. */
html.es-shell-v2 body.editor-mode .editor-back {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 38px;
  padding: 0 16px 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-white);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
  text-decoration: none;
  transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
html.es-shell-v2 body.editor-mode .editor-back:hover {
  color: var(--text-primary);
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
html.es-shell-v2 body.editor-mode .editor-back:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.4);
}

/* Save status, restyled as a pill from its new parent only — setSaveStatus() writes
   textContent and toggles .hidden, so no JS change is needed. */
html.es-shell-v2 body.editor-mode #editor-bar .save-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  /* Was --bg-tertiary (#f3f2ef) — the cream that reads as dirty white against the
     panel. White on the canvas separates cleanly and matches the pill beside it. */
  background: var(--surface-white);
  border: 1px solid var(--border);
  color: var(--text-tertiary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}
html.es-shell-v2 body.editor-mode #editor-bar .save-status::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--success);
  flex-shrink: 0;
}
html.es-shell-v2 body.editor-mode #editor-bar .save-status--error::before {
  background: var(--danger);
}
/* Load-bearing. .hidden is a bare `display:none` with no !important, so the rule above
   outranks it and the pill would sit there empty until the first autosave. The same
   trap applies to every JS-toggled control in this bar. */
html.es-shell-v2 body.editor-mode #editor-bar .save-status.hidden,
html.es-shell-v2 body.editor-mode #editor-bar-actions .btn.hidden { display: none; }

/* --- the canvas stops being a card --------------------------------------------- */
/* border-color rather than border:0 — under border-box the outer size is the same
   either way, but this keeps canvasWrapper.clientWidth bit-identical, and imageScale
   is derived from it. border-radius stays: #canvas-empty-state inherits it for the
   dashed drop target, which still has to read as one. */
html.es-shell-v2 body.editor-mode .canvas-container {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

/* Anchored to the old header height, which no longer exists here. */
html.es-shell-v2 body.editor-mode .guest-restore-banner {
  top: calc(var(--editor-bar-height) + var(--spacing-sm));
}

/* Plain `stable`, NOT `both-edges`. both-edges reserved an empty 15px strip on the
   left to mirror the scrollbar on the right, which made the two insets equal on paper
   and wrong to the eye: on the right the strip is filled by the scrollbar, on the left
   it is just a gap, so the left read as 15px roomier. With plain `stable` the left
   control sits 28px from the window and the right one sits 28px from the scrollbar —
   equal gaps to the nearest visible thing, which is what is actually perceived. */
html.es-shell-v2:has(body.editor-mode) { scrollbar-gutter: stable; }

/* --- Entering a project ---------------------------------------------------------
   Not a cross-fade: the workspace pushes past the viewer while the editor expands into
   place, so it reads as moving INTO the project rather than swapping two pictures.
   Both halves are short — this happens every time a card is clicked. */
html.es-shell-v2 .es-enter-editor #landing-view {
  animation: es-editor-depart 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
}
html.es-shell-v2 .es-enter-editor #editor-screen,
html.es-shell-v2 .es-enter-editor #editor-bar {
  animation: es-editor-arrive 0.26s cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes es-editor-depart {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(1.04); }
}
@keyframes es-editor-arrive {
  from { opacity: 0; transform: scale(0.985); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  html.es-shell-v2 .es-enter-editor #landing-view,
  html.es-shell-v2 .es-enter-editor #editor-screen,
  html.es-shell-v2 .es-enter-editor #editor-bar { animation: none; }
}

/* --- Undo / redo, and the panel ------------------------------------------------- */
/* .btn-gray is the creamy grey that reads as dirty against the new canvas. White with
   a hairline, and rounder — these are the two controls you press most while slicing. */
html.es-shell-v2 body.editor-mode #editor-bar-actions .btn-gray {
  /* Rounded squares, not rounded rectangles: 40px with a 14px radius is the ratio in
     the reference — soft enough to read as a pair of tiles, square enough that the
     arrow inside still has room. Height comes from the shared .btn rule. */
  width: 40px;
  padding: 0;
  background: var(--surface-white);
  border: 1px solid var(--border);
  border-radius: 14px;
  color: var(--text-secondary);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
html.es-shell-v2 body.editor-mode #editor-bar-actions .btn-gray svg {
  width: 19px;
  height: 19px;
}
html.es-shell-v2 body.editor-mode #editor-bar-actions .btn-gray:hover:not(:disabled) {
  background: var(--surface-white);
  color: var(--text-primary);
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
/* One place owns the height. Three rules previously declared it at identical
   specificity, so only the last had any effect — a change to either of the others
   would have silently done nothing. */
html.es-shell-v2 body.editor-mode #editor-bar-actions .btn { height: 40px; }

/* Left and right insets match: the back button and the New Image button sit the same
   distance from their respective window edges.
   The zoom pill is position:fixed, so it cannot inherit the bar's padding and has to
   be told the same number. Just the bleed, with no gutter correction: a fixed element's
   containing block already starts inside the scrollbar gutter, so it lands level with
   the padded content on every platform — including macOS, where the gutter is zero. */
html.es-shell-v2 body.editor-mode #command-bar {
  left: var(--editor-bleed-x);
}

/* No marketing footer on a project page — same call already made for the workspace. */
html.es-shell-v2 body.editor-mode .site-footer { display: none; }

/* The right column stops being a scroller. It was `overflow-y: auto` with a viewport
   max-height, so the moment its content passed that height the WHOLE column scrolled —
   taking the brand chip out of view and leaving the card clipped mid-shadow. The brand
   selector is a fixed header for this column and must never move.
   If anything needs to scroll it is the slice list, which has its own box below. */
html.es-shell-v2 body.editor-mode .editor-right {
  max-height: none;
  overflow: visible;
}
/* Only the slices scroll, and only once there are genuinely too many to fit.
   fitSnippetListToViewport() (js/editor/canvas.js) measures the real space left below
   the list and sets --snippet-list-max; the calc() is the no-JS fallback, and it has
   to reserve for the tallest possible block above the list, which is why it wastes
   space on a tall screen. */
html.es-shell-v2 body.editor-mode #snippet-list {
  max-height: var(--snippet-list-max, calc(100vh - var(--editor-content-offset) - 320px));
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Lifted, so the panel reads as sitting above the canvas rather than printed on it. */
html.es-shell-v2 body.editor-mode .snippets-section {
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
  border: 1px solid var(--border);
}

/* --- Export button: brand indigo, not green ---------------------------------------
   The green predates the palette and was the only saturated non-indigo left in the
   app. It is the primary action of the whole editor, so it should carry the primary
   colour — and "send" reading as green implied a success state before anything had
   happened. */
/* At 0 exports the primary steps down to the app's SECONDARY button: the same white
   surface, hairline border, secondary text and small shadow as every .btn-white in
   the product. It stays clickable (the click opens the wall) but stops wearing the
   gradient of an action it cannot deliver - and the indigo primary has moved into
   the wall's own CTA, where it belongs.

   Two earlier attempts are worth not repeating: a slate fill (--border used as a
   background, which is why it matched nothing on screen), and an opacity fade - the
   button's reveal animation writes `opacity: 1` inline, so a stylesheet opacity can
   never win against it. */
html.es-shell-v2 body.editor-mode #download-all.btn-download-all.is-exhausted {
  background-image: none;
  background-color: var(--bg-elevated);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
html.es-shell-v2 body.editor-mode #download-all.btn-download-all.is-exhausted:hover {
  filter: none;
  transform: none;
  background-color: var(--bg-secondary);
  box-shadow: var(--shadow-md);
}
html.es-shell-v2 body.editor-mode #download-all.btn-download-all.is-exhausted svg { color: var(--text-tertiary); }
html.es-shell-v2 body.editor-mode #download-all.btn-download-all {
  background-image: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
  box-shadow: 0 4px 12px -2px rgba(var(--primary-rgb), 0.4);
}
html.es-shell-v2 body.editor-mode #download-all.btn-download-all:hover:not(:disabled) {
  box-shadow: 0 6px 18px -2px rgba(var(--primary-rgb), 0.5);
}

/* The brand menu opened UNDERNEATH the zoom pill: the menu is z-index 50 and
   #command-bar is 100, and now that the chip lives top-left the two occupy the same
   column. A transient overlay has to win against permanent furniture. */
html.es-shell-v2 body.editor-mode #editor-bar .brand-export-menu { z-index: 130; }


/* --- Editor type, dialled back --------------------------------------------------
   Measured against the rest of the bar rather than adjusted by feel. The controls sit
   on open canvas now, with no header band to sit against, so several of them were
   carrying more weight than their job deserves. */

/* Getting out is a quiet action, not a headline. */
html.es-shell-v2 body.editor-mode .editor-back { font-size: var(--text-sm); }

/* "Saved" is an ambient status — it should never out-shout the brand it sits above.
   It was 13/500 against the brand label's 13/600 and still read heavier because of the
   pill around it, so it drops a size and loses the pill's contrast. */
html.es-shell-v2 body.editor-mode #editor-bar .save-status {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  padding: 3px 9px;
}

/* Slice names. 700 read as headings, 500 washed out — semibold at the smaller size is
   the middle: the original weight, carried at 14px instead of 15, so the row reads as
   solid without shouting. */
html.es-shell-v2 body.editor-mode .snippet-title {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

/* The zoom readout is a value you glance at, and it sits alone on the canvas. */
html.es-shell-v2 body.editor-mode #command-bar #zoom-display { font-size: var(--text-xs); }

/* --- Corners, reconciled with the workspace --------------------------------------
   The workspace obeys the ladder in this file — frame 18, block 12, control 10 — and
   the editor had drifted off it in both directions. */

/* 14 on a 40px button is rounder than the workspace's 42px primary at 10, which is why
   one looked right and the other did not. Same step, now. */
html.es-shell-v2 body.editor-mode #editor-bar-actions .btn-primary {
  border-radius: var(--radius-md);
}
/* Undo/redo stay at 14, deliberately. They are 40px SQUARES, and a square needs a
   larger radius than a wide button to read as equally round — matching the number
   would make them look sharper than the button beside them. */

/* The snippets panel is this page's frame, the same role the drop box plays on the
   workspace, so it takes the frame step rather than the block step it had. */
html.es-shell-v2 body.editor-mode .snippets-section {
  border-radius: var(--radius-xl);
}
/* Send-to was at 8 — below even the control step, on the largest button in the app.
   12 as a block: it is a 46px full-width panel action, not a toolbar control. */
html.es-shell-v2 body.editor-mode #download-all.btn-download-all {
  border-radius: var(--radius-lg);
  /* 46 -> 48. It is the one button the whole editor exists to reach, and at 46 it was
     thinner than the 12px radius wanted — the corners were eating enough of the edge
     that the bar between them read as slight. Also clears the 40px controls in the top
     bar, so the primary action is visibly the heaviest thing on the page. */
  min-height: 48px;
}
/* Slice rows stay 12: blocks nested inside the 18 frame, one step down as the ladder
   requires. */

/* The slices label. .ui-section-label supplies the 11px semibold uppercase micro-label
   already used on the project cards, so this only positions it. */
html.es-shell-v2 body.editor-mode .snippets-label {
  margin: var(--spacing-md) 0 8px;
  color: var(--text-tertiary);
}
html.es-shell-v2 body.editor-mode .snippets-label.hidden { display: none; }


/* --- The footer handle carries an editable value ---------------------------------
   Drag for approximate, type for exact. The input is styled as part of the handle
   rather than a form field parked on it: no chrome at rest, a faint well on hover, a
   solid one on focus. Sized in ch so four digits fit without the handle jumping. */
html.es-shell-v2 body.editor-mode .footer-crop-input {
  /* Was 4.5ch / 1px padding — a 31px target you could barely hit. A field you are
     meant to type into needs to be a real target, not a caption that happens to
     accept focus. */
  width: 6ch;
  min-height: 26px;
  /* The value is what the control is about, so it steps one rung above the uppercase
     label rather than sharing its size. */
  font-size: var(--text-2xs);
  margin-left: 5px;
  padding: 2px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.14);
  color: inherit;
  font-family: inherit;
  font-weight: inherit;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  /* Centred, not right-aligned. Right-alignment is for columns of figures you scan
     down; this is a single value in a fixed 6ch box, where it just read as pushed
     against one wall. Tabular figures keep it from wobbling as digits change. */
  text-align: center;
  cursor: text;
  transition: background 0.12s ease, border-color 0.12s ease;
  /* Spinners would crowd a control this small, and dragging already covers nudging. */
  -moz-appearance: textfield;
  appearance: textfield;
}
html.es-shell-v2 body.editor-mode .footer-crop-input::-webkit-outer-spin-button,
html.es-shell-v2 body.editor-mode .footer-crop-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
html.es-shell-v2 body.editor-mode .footer-crop-input:hover { background: rgba(255,255,255,0.24); }
html.es-shell-v2 body.editor-mode .footer-crop-input:focus {
  outline: none;
  background: var(--surface-white);
  border-color: var(--surface-white);
  color: var(--text-primary);
}
html.es-shell-v2 body.editor-mode .footer-crop-unit { margin-left: 1px; opacity: 0.75; }

/* Only present when the value differs from the brand default, so it is never
   decoration — it appears at the one moment saving means something. */
html.es-shell-v2 body.editor-mode .footer-save-brand {
  margin-left: 7px;
  min-height: 26px;
  padding: 3px 14px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-white);
  color: var(--primary-text);
  font: inherit;
  font-weight: var(--weight-semibold);
  cursor: pointer;
  white-space: nowrap;
}
html.es-shell-v2 body.editor-mode .footer-save-brand:hover { background: var(--app-shell-hover); }
html.es-shell-v2 body.editor-mode .footer-save-brand.hidden { display: none; }

/* The only heading in the panel, so it carries the section rather than captioning it. */
html.es-shell-v2 body.editor-mode .snippets-label {
  font-size: var(--text-sm);
  letter-spacing: 0.05em;
  color: var(--text-secondary);
  margin: var(--spacing-lg) 0 10px;
}


/* The handle has to hold a label, a field and sometimes a button, so it needs real
   padding — at the old size everything inside it was touching once Save appeared. */
html.es-shell-v2 body.editor-mode .footer-handle {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  /* Shorter: at 42px it stood taller than it needed to on top of the artwork. The
     horizontal padding runs wider than the internal gaps on purpose — the parts read
     as one control, with the air pushed to the outside instead of between them. */
  padding: 4px 11px 4px 16px;
  white-space: nowrap;
  /* Set by syncFooterHandleScale() — tracks zoom at half rate so the pill never
     outgrows the email at 50% or looks lost at 150%. */
  transform: translate(-50%, -50%) scale(var(--crop-scale, 1));
  transform-origin: center center;
}

/* Now that the crop lives on the canvas, its old home is redundant: the field, the
   lock and "Save as new default" all move out of the brand menu, leaving it to do the
   one job its name promises. The elements stay in the DOM — setFooterHeight() writes
   to #exclude-height, and the canvas Save button reuses #footer-save-default-btn's
   handler, so removing them outright would break both. */
html.es-shell-v2 body.editor-mode .brand-menu-section:has(#footer-settings-group) {
  display: none;
}
/* With the crop gone from the menu, the chip is purely a brand switcher, so the little
   settings cog on it is describing something that is no longer there. */
html.es-shell-v2 body.editor-mode .brand-chip-settings-icon { display: none; }

/* Slice rows: a touch larger, and vertically centred against the action buttons —
   the title sat on the text baseline while the icons centred, so the row read as
   slightly off. */
html.es-shell-v2 body.editor-mode .snippet-title { font-size: var(--text-md); }
html.es-shell-v2 body.editor-mode .snippet-card-header,
html.es-shell-v2 body.editor-mode .snippet-title-group { align-items: center; }
/* The header's 4px bottom margin was separating it from .snippet-subtitle, which is
   display:none — so it only ever pushed the one remaining row 4px above the card's
   centre (11px of space above the text, 15px below). Dead spacing with a visible cost. */
html.es-shell-v2 body.editor-mode .snippet-card-header { margin-bottom: 0; }


/* --- Skeleton geometry must equal the real thing ---------------------------------
   The boot skeleton thumb is 140px (workspace.css) while the real shell thumb is
   150px, so every card grew 10px the moment data landed — a whole-grid shift. Same
   for the corner radius, which stepped from 14px to 12px. */
html.es-shell-v2 body.es-shell-flat .history-item__skeleton-thumb {
  height: 150px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}


/* Shorter controls to match the shorter pill. */
html.es-shell-v2 body.editor-mode .footer-crop-input,
html.es-shell-v2 body.editor-mode .footer-save-brand { min-height: 24px; }

/* Brand list: 176px is not a whole number of rows, so the last brand was always
   sliced in half and the padding read as lopsided — lots of air at the top, none at
   the bottom. Even padding, and a max-height that ends between rows. */
html.es-shell-v2 body.editor-mode .brand-export-menu #brand-list-container {
  max-height: 208px;
  padding: 4px;
  scrollbar-width: thin;
}

/* Brand chip mark — the brand's colour swatch, matching the workspace rail, in place
   of the old green "selected" dot. Green now belongs solely to the save-status pill
   sitting a few pixels away. Sized and rounded like the condensed rail mark so the
   two read as the same object in two places. */
html.es-shell-v2 #brand-indicator-dot.brand-chip-mark {
  width: 15px;
  height: 15px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
/* No brand yet (or signed out): an outline, not a filled grey square, so it reads as
   an empty slot rather than a brand whose colour happens to be grey. */
html.es-shell-v2 #brand-indicator-dot.brand-chip-mark.is-empty {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--border-strong);
}

/* Brand chip: nothing rather than a wrong thing while the brand list is in flight.
   #active-brand-label's markup default is "Footer crop" — the correct label for an
   anonymous user, but a lie for a signed-in one, and it is what a project restore
   showed for the whole spinner. Until klaviyo.js marks the chip resolved, the label
   is replaced by a skeleton bar of roughly a brand name's width, so the chip holds
   its shape and never asserts a mode it does not know yet. */
html.es-shell-v2.brand-chip-pending body.editor-mode #brand-dropdown-trigger #active-brand-label {
  visibility: hidden;
  position: relative;
}
html.es-shell-v2.brand-chip-pending body.editor-mode #brand-dropdown-trigger #active-brand-label::after {
  content: "";
  visibility: visible;
  position: absolute;
  inset: 50% auto auto 0;
  transform: translateY(-50%);
  width: 62px;
  height: 11px;
  border-radius: var(--radius-sm);
  background-image: linear-gradient(90deg, rgba(var(--skeleton-tint), 0.075) 25%, rgba(var(--skeleton-tint), 0.14) 37%, rgba(var(--skeleton-tint), 0.075) 63%);
  background-size: 400% 100%;
  animation: es-skeleton-shimmer-boot 1.4s ease-in-out infinite;
}
/* The chevron would invite a click on a control that cannot answer yet. */
html.es-shell-v2.brand-chip-pending body.editor-mode #brand-dropdown-trigger .brand-chip-chevron {
  opacity: 0;
}

/* --- Signed-out account controls in the editor bar --------------------------------
   Hidden by default everywhere, so flag-off and every signed-in path are untouched and
   a failure to evaluate the session can only ever hide it, never show a signup prompt
   to someone who already has an account. The session classes come from index.php
   before any bytes are sent, so this resolves on the first painted frame rather than
   flashing in after check_session returns. */
.editor-guest-auth { display: none; }
html.es-shell-v2:not(.es-session-logged-in):not(.logged-in-early)
  body.editor-mode:not(.logged-in) .editor-guest-auth {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-sm);
  /* After the relocated undo / redo / New Image, separated by a hairline: editor tools
     are one group, the account is another. The divider also stops "Start free" and
     "New Image" — both indigo — from sitting shoulder to shoulder and competing. */
  order: 2;
  margin-left: 2px;
  padding-left: var(--spacing-md);
  border-left: 1px solid var(--border);
}
html.es-shell-v2 body.editor-mode .editor-guest-auth__login,
html.es-shell-v2 body.editor-mode .editor-guest-auth__cta {
  height: 40px;                 /* matches #editor-bar-actions .btn */
  padding: 0 16px;
  border-radius: var(--radius-pill);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.12s ease, box-shadow 0.12s ease, color 0.12s ease;
}
/* Quiet: it is for people who already have an account and know they want it. */
html.es-shell-v2 body.editor-mode .editor-guest-auth__login {
  border: 0;
  background: none;
  color: var(--text-secondary);
}
html.es-shell-v2 body.editor-mode .editor-guest-auth__login:hover {
  background: var(--surface-wash);
  color: var(--text-primary);
}
html.es-shell-v2 body.editor-mode .editor-guest-auth__cta {
  border: 0;
  background: var(--primary);
  color: var(--surface-white);
  box-shadow: 0 1px 2px rgba(var(--primary-rgb), 0.28);
}
html.es-shell-v2 body.editor-mode .editor-guest-auth__cta:hover {
  background: var(--primary-dark);
  box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
}
html.es-shell-v2 body.editor-mode .editor-guest-auth__cta:focus-visible,
html.es-shell-v2 body.editor-mode .editor-guest-auth__login:focus-visible {
  outline: 2px solid rgba(var(--primary-rgb), 0.45);
  outline-offset: 2px;
}
