
  /* Color tokens for the plain-CSS exceptions in this file (the workflow
     canvas, hand-rolled tables/cards/chips -- see the top-of-file comment
     for why these exist at all). Not a rip-and-replace of every literal
     below -- these are the values already hand-matched to Polaris via live
     testing (see each rule's own history in the comments throughout this
     file), just given names so new additions have a single source instead
     of another one-off hex literal. Existing literals are migrated to
     these as their surrounding rules get touched, not all at once here. */
  :root {
    --fp-color-border: #e3e3e3;

    /* Corner radius scale, anchored to what Polaris actually paints.
       Measured, not guessed: an <s-section>'s own painted box computes to
       border-radius 12px, padding 16px, and NO border (it uses a shadow).
       This app's own boxes had drifted to nine different radii -- 3, 4, 5,
       6, 7, 8, 10, 12 and 20px -- with card surfaces mostly at 8px and a
       1px border, so every FlowPrint card sat next to a Shopify card
       looking flatter and harder-edged. That mismatch is what reads as
       "square corners", and it is why repeatedly restyling <s-section>
       never fixed anything: s-section was correct the whole time.

       card    matches s-section exactly, for any white surface that IS a
               card in its own right
       control tiles, inputs and buttons sitting INSIDE a card, which
               should read as nested rather than competing with it
       small   chips and tight controls */
    --fp-radius-card: 12px;
    --fp-radius-control: 8px;
    --fp-radius-small: 6px;
    --fp-color-subdued: #6b6b6b;
    --fp-color-success: #008060;
    --fp-color-critical: #d82c0d;
    --fp-color-brand: #006e52;
    --fp-focus-ring: #2c6ecb;
  }

  /* Skip link -- accessibility best practice (shopify.dev/docs/apps/build/
     accessibility, "Global": a visible-when-focused skip link past the nav
     to the main content). Off-screen until focused, matching the standard
     pattern (absolute-position far off-canvas, snap into view on :focus). */
  .fp-skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 2000;
    background: white; color: #202223; padding: 10px 16px; border-radius: 0 0 8px 0;
    font-size: 14px; font-weight: 600; text-decoration: none;
    border: 2px solid var(--fp-focus-ring);
  }
  .fp-skip-link:focus { left: 0; }

  /* Screen-reader-only, visually hidden -- standard clip-based pattern
     (not display:none, which would also hide it from assistive tech). */
  .fp-visually-hidden {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }

  /* Keyboard-focus affordance for every hand-rolled interactive element in
     this file (see the top-of-file comment: these exist because no Polaris
     component covers them). Real components (s-button, s-clickable, etc.)
     handle their own focus styling internally; these are specifically the
     plain <button>/<div>-as-button elements this file authors itself,
     which had no focus indicator at all before this -- confirmed absent
     (accessibility audit, 2026-08-12): only three bare :hover rules existed
     in the whole stylesheet and no :focus-visible rule anywhere. */
  .fp-starter-card:focus-visible,
  .fp-flow-card-remove:focus-visible,
  .fp-flow-add:focus-visible,
  .fp-flow-card.fp-flow-clickable:focus-visible {
    outline: 2px solid var(--fp-focus-ring); outline-offset: 2px;
  }
  /* .fp-quickaction is an <s-clickable> host, not a plain element like the
     three above -- given this file's repeated, documented history of
     <s-clickable>'s box drawing inside its shadow root rather than on the
     light-DOM host (border/background props, see the .fp-quickaction
     comment further down), this outline rule is NOT yet confirmed live to
     actually render visibly on real focus. Left in since it's a correct,
     harmless attempt (an outline on a host element is a different
     mechanism than the border/background prop issue -- it doesn't depend
     on the shadow root cooperating), but verify with a real keyboard tab
     through the Quick actions row before trusting it; if it's invisible,
     this is the same class of bug as everything else in this file and
     needs the same fix (a plain-div wrapper carrying its own visible
     focus state instead of relying on the component host). */
  .fp-quickaction:focus-visible {
    outline: 2px solid var(--fp-focus-ring); outline-offset: 2px;
  }

  /* Polish pass (2026-08-13): a shared, subtle hover/press language for
     every hand-rolled clickable surface in this file -- none of them had
     any transition at all before this (confirmed: zero "transition"
     declarations existed anywhere), so every hover/focus state above
     snapped instantly, which reads as unfinished next to the rest of the
     Shopify admin's own soft, deliberate motion. --fp-motion is one
     shared timing so every surface feels consistent, not each tuned
     separately. Respects prefers-reduced-motion by disabling all of it
     for users who've asked for that, same standard as any other real
     Shopify admin surface. */
  :root { --fp-motion: 150ms cubic-bezier(0.2, 0, 0, 1); }
  @media (prefers-reduced-motion: reduce) {
    :root { --fp-motion: 0ms; }
  }
  .fp-quickaction {
    transition: transform var(--fp-motion), box-shadow var(--fp-motion);
  }
  .fp-quickaction:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 14px rgba(0,0,0,0.10);
  }
  .fp-quickaction:active { transform: translateY(0); }

  /* Inter-section spacing.
     
     This rule used to be a margin-block-end on .fp-block, described as
     "explicit, guaranteed" spacing. It was neither: measured in a real
     browser, an <s-section> host computes to display: contents, which
     generates no box at all, so a margin on it is silently dropped. The
     even spacing on a normal page has always come from <s-page>'s own
     layout, and this rule contributed nothing to it.
     
     That matters for the wrapper cases. Two blocks (the workflows
     list/canvas toggle, the template edit form) sit inside a plain div
     carrying their own open/close state, so they are not direct <s-page>
     children and do not get its spacing -- and the margin that was
     supposed to cover them could never have worked. A gap on the PARENT
     does work, because a display: contents child hands its own children
     up into the parent's layout. That is the same fix already proven on
     the Settings page (.fp-settings-main below); it is applied to the
     wrappers here rather than left to a margin that cannot apply. */
  .fp-block-group { display: flex; flex-direction: column; gap: 16px; }
  /* The card wrapper is a plain div precisely so this margin applies.
     The <s-box> inside it computes to display: contents and would
     drop a margin silently, which is what removed every gap between
     cards the moment they stopped being <s-section>s. */
  .fp-block { margin-block-end: 16px; }

  /* Real, guaranteed card chrome for content that sits inside a plain
     visibility-toggle wrapper div (tpl-legacy-view / tpl-block-editor,
     src/ui/embedded.ts) rather than as a direct <s-page> child -- this
     file's own comment right above already flagged that exact shape
     ("two blocks here ... need a wrapper div carrying their own open/
     close state, so they can't fully match [<s-page>'s] pattern") but
     had never actually hit the consequence until a live screenshot
     review (2026-08-13) showed it: <s-section> nested one level inside
     such a wrapper does NOT reliably draw its own card background/
     border/shadow -- confirmed by comparing two structurally-identical
     <s-section class="fp-block" padding="base"> blocks on the same
     page, where the ones that worked (the block editor's toolbar/
     layout) only had a visible boundary because THEIR inner content
     (.fp-block-palette/.fp-block-props) carries its own explicit CSS
     border, not because <s-section> drew one. Same lesson as every
     other Polaris-shadow-DOM gotcha in this file: don't trust the
     component's own chrome here, draw it for real. */
  .fp-editor-card {
    background: white; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-card);
    padding: 16px; margin-block-end: 12px;
  }
  /* The legacy editor's card stacked a banner, a button and the form
     with no rhythm -- the banner sat flush on the button, the button on
     the fields (seen live 2026-09-05). One flex gap fixes the whole
     column; scoped so the block editor's own layout is untouched. */
  #tpl-legacy-view .fp-editor-card { display: flex; flex-direction: column; gap: 14px; }
  /* The starters holder is an EMPTY div whenever an existing template is
     open -- in a flex column it still earns a gap either side, which
     stacked ~28px of dead air above the system-template notice while
     the sides sat at 16 (reported 2026-09-05, correctly, as not
     uniform). Empty children simply don't play. */
  #tpl-legacy-view .fp-editor-card > div:empty { display: none; }
  #tpl-legacy-view .fp-editor-card > s-button { align-self: flex-start; }

  /* Plain <div>s, not <s-stack>/<s-text>/<s-paragraph>, for the tight
     two-line heading+subtext pairs in the "Nothing is stored" and "Help
     and support" cards. Two things were tried and confirmed live to do
     nothing: an "s-paragraph { margin: 0 }" rule (shadow DOM
     encapsulation means page-level CSS can't reach the real <p> it
     renders internally) and swapping just the paragraph for a plain div
     while keeping the surrounding <s-stack gap="small-100"> (the huge gap
     persisted regardless, meaning the stack itself -- not just the
     paragraph -- wasn't producing the small gap its own gap="small-100"
     prop implies). Dropping the custom stack/text elements entirely for
     just these two lines and controlling spacing with plain margin is the
     only version of this actually confirmed to produce a small gap.
     Font size/color hand-matched to Polaris's own heading/subdued body
     text. */
  .fp-subtext-heading { margin: 0; font-size: 14px; font-weight: 600; }
  .fp-subtext { margin: 2px 0 0; font-size: 14px; line-height: 1.4; color: #6b6b6b; }

  /* inlineSize="base" on <s-page> (Home only, below) is the documented,
     correct way to get a narrower-than-full-bleed layout, but Shopify's
     inlineSize presets ("small" | "base" | "large") don't map to any
     literal percentage -- confirmed live that "base" alone didn't produce
     a visually obvious margin at typical desktop widths. This locks in an
     explicit 10% gutter on each side on top of it. Scoped to a class, not
     the shared #fp-main id every page's root <s-page> now carries (2026-
     08-12 multi-page rebuild) -- Workflows/Templates/Job log are index
     pages and must render full width (Built for Shopify 4.1), so only
     Home's dashboard opts into this narrower layout. */
  /* Home's width comes from <s-page inlineSize="base">, which is
     Polaris's own API for it. This used to ALSO carry
     "max-width: 80%; margin-inline: auto", an external override of a
     Polaris layout component's own box -- the exact thing that never
     works reliably here (see the shadow-DOM note at the top of this
     file) and the only such override left in the app.
     
     It is removed rather than kept as a harmless duplicate because it is
     not provably harmless off this machine: on desktop s-page computes to
     display: contents, so the max-width is dropped and the rule does
     nothing, but that is a property of the Polaris build being served.
     A build where s-page is a real box would apply it, constraining the
     page and changing how Polaris sizes what is inside it. Declaring the
     width once, through the component's own prop, removes that variable. */
  .fp-page-narrow { margin-inline: auto; }

  /* Small live-status indicator for the worker table -- Polaris has no
     "pulsing" affordance of its own (s-badge is static), so this is a
     deliberate minor exception, same reasoning as the workflow canvas:
     nothing documented covers it, and it's used consistently everywhere
     a worker's connection state appears. Only "paired" (actively
     connected) pulses; pending/revoked show a static dot. */
  .fp-status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-inline-end: 6px; vertical-align: middle; }
  /* Dot and badge on one baseline -- unwrapped, the dot rendered as its
     own stray line above the badge in the Workers table. */
  .fp-worker-statusline { display: inline-flex; align-items: center; }
  .fp-status-dot.fp-dot-success { background: #008060; }
  .fp-status-dot.fp-dot-warning { background: #b98900; }
  .fp-status-dot.fp-dot-neutral { background: #8a8a8a; }
  .fp-status-dot.fp-dot-pulse { animation: fp-status-pulse 1.6s ease-in-out infinite; }
  @keyframes fp-status-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(0, 128, 96, 0.45); }
    50% { box-shadow: 0 0 0 5px rgba(0, 128, 96, 0); }
  }

  /* Branded logo mark -- ICON_BOLT_BRAND draws its own dark rounded
     square + real bolt polygon (matching flowprint-worker's tray icon
     pixel-for-pixel), so this wrapper is just a sizing/clipping box, not
     a second background layer. s-box's background prop only offers
     neutral admin grays, which is why the mark itself needs to be a
     contained custom-CSS/SVG exception rather than the whole page
     fighting Polaris's palette. */
  .fp-logo-badge { width: 44px; height: 44px; border-radius: 12px; overflow: hidden; flex-shrink: 0; line-height: 0; }

  /* Empty states for the resource tables -- an icon, a heading, a short
     explanation, and the same CTA the intro card above already offers,
     spanning every column via grid-column so it doesn't look like a
     stray one-column row in a multi-column grid. */
  .fp-empty-state { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px; padding: 40px 20px; }
  /* The explanation should read as a sentence, not a column of
     narrow fragments, so it gets a measure rather than filling the
     whole table width. */
  .fp-empty-state s-paragraph { max-width: 46ch; }
  .fp-empty-state s-button { margin-block-start: 8px; }
  .fp-empty-state s-icon { opacity: 0.5; }

  /* Home metrics strip -- plain CSS grid + a real @media query, not
     <s-grid gridTemplateColumns="@container ..."> (what this used to be).
     Confirmed live on a real phone (screenshot from the actual Shopify
     mobile app, not just a resized desktop browser): the container-query
     syntax never switched to the narrow layout at all, so the stat cards
     stayed squeezed into one row on a phone-width screen -- "Workflows
     enabled" got clipped mid-word and the usage/status badges truncated
     to "0..." and "L.". CSS Container Queries need an ancestor with
     container-type set for @container to evaluate against anything;
     <s-grid> never establishes one, so the condition had nothing real to
     measure and just fell through to the wide branch every time -- same
     class of "Polaris prop doesn't reliably do the CSS trick its value
     implies" issue as .fp-grid-table/.fp-plan-card elsewhere in this
     file. A real @media query needs no component cooperation at all.
     Card-style equal grid now, same pattern as .fp-quickaction-grid
     above (3 desktop / 2 mobile columns) instead of a single divided
     row hardcoded to exactly 4 stats -- adding the outstanding-quotes
     stat made that count 5, and a real card grid handles any count
     without a bespoke divider/column formula per stat total. */
  .fp-metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
  /* Setup-guide rows (storefront status, review fix): full-width click
     target, state icon + name left, state text + chevron right. */
  .fp-guide-row { display: block; }
  .fp-guide-row-head {
    display: flex; align-items: center; gap: 10px; width: 100%;
    background: none; border: 0; padding: 7px 4px; cursor: pointer;
    font: inherit; text-align: left; border-radius: 8px;
  }
  .fp-guide-row-head:hover { background: #f6f6f7; }
  .fp-guide-row-head:focus-visible { outline: 2px solid var(--fp-color-focus); outline-offset: 1px; }
  .fp-guide-row-title { font-weight: 600; color: #1a1c1e; }
  .fp-guide-row-state { margin-left: auto; font-size: 12px; color: var(--fp-color-subdued); }
  .fp-guide-chevron { color: var(--fp-color-subdued); transition: transform 120ms ease; }
  .fp-guide-open .fp-guide-chevron { transform: rotate(180deg); }
  .fp-guide-row-body { padding: 0 4px 10px 34px; display: flex; flex-direction: column; gap: 8px; }
  /* [hidden] loses to any class that sets display -- assert it explicitly
     (same bug class as the quote window's pickup list, 2026-08-27). */
  .fp-guide-row-body[hidden] { display: none; }
  .fp-guide-row-actions { display: flex; }

  /* Metrics-card values read as numbers, not body copy. */
  .fp-metric-value { font-size: 16px; line-height: 20px; }

  .fp-metric-cell { display: flex; }
  .fp-metric { flex: 1 1 auto; box-sizing: border-box; }
  @media (max-width: 900px) {
    .fp-metrics-grid { grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 420px) {
    .fp-metrics-grid { grid-template-columns: 1fr; }
  }

  /* Equal-width, equal-height tiles on a real, deterministic column count
     -- CSS Grid, not the earlier flexbox-wrap version. That version's
     flex: 1 1 220px basis produced a different, screen-width-dependent
     column count on every desktop viewport (however many 220px tiles
     happened to fit), so the tiles were only ever "equal" within a given
     width, never a stable grid -- and any trailing partial row was a
     single stretched-wide orphan. Fixed column counts (3 desktop / 2
     mobile, matching the metrics grid below) divide the current 6-tile
     set evenly at both breakpoints with no orphan at all, and every cell
     in a row is identically sized because grid columns are, unlike flex
     bases, always equal-width by construction. If a future tile count
     doesn't divide evenly, the trailing row just left-aligns rather than
     stretching -- acceptable, and simpler than reviving the grid-column
     span trick that didn't visibly apply live last time it was tried
     (see git history) for what was a flexbox-specific problem anyway. */
  .fp-quickaction-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
  .fp-quickaction-cell { display: flex; }
  @media (max-width: 900px) {
    .fp-quickaction-grid { grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 420px) {
    .fp-quickaction-grid { grid-template-columns: 1fr; }
  }
  /* Plain div, not <s-stack direction="inline">: the longest label ("New
     email automation") kept wrapping its icon above the text instead of
     staying side-by-side like its shorter siblings, even after adding
     flex-wrap:nowrap directly on the <s-stack> -- which had no visible
     effect, confirmed live twice. That's the same lesson the border fix
     above already relearned the hard way: a prop/behavior exposed on a
     Polaris component's HOST element doesn't mean the host's own box is
     what's actually laying things out -- <s-stack>'s real flex container
     lives inside its shadow root, so overriding flex-wrap on the light-
     DOM host had nothing to act on. A plain div with real, own CSS
     flex-wrap:nowrap is guaranteed to work, same reasoning already
     applied to .fp-row-actions for the identical class of wrap bug. This
     was also why the tile was taller than its row-mates -- two lines of
     wrapped content instead of one is what set the stretched row height
     for everything else. */
  .fp-quickaction-row { display: flex; flex-wrap: nowrap; align-items: center; gap: 12px; }
  /* Theme block/embed status on Home (BFS 4.2.3). A vertical list on
     purpose: the quick-action row above is flex-wrap:nowrap, so reusing
     it for a variable number of block names would scroll sideways on a
     phone, which is its own rejection reason (4.1.2). */
  .fp-ext-status-list { display: grid; gap: 8px; }
  .fp-ext-status-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  /* Reverted, 2026-08-05: one tile was seen live with no visible border,
     which was assumed to mean the border="base"/background="base" props
     weren't reliably reflected -- so an external CSS border/background
     was added as a "guaranteed" fallback. That was the wrong diagnosis:
     the component's own props draw their box on an element *inside* its
     shadow root, not the light-DOM host itself, so the external CSS
     border didn't replace anything -- it became a second, separate box
     nested just outside the component's own, visible live as a doubled
     outline on every tile once the fix shipped. The props were working
     correctly all along. Whatever caused the one tile's border to be
     missing in the original report wasn't reproducible afterward and is
     more likely a one-off render timing/upgrade hiccup than a persistent
     CSS issue -- if it recurs, investigate the actual cause rather than
     re-adding a competing external box. */
  .fp-quickaction { cursor: pointer; display: block; flex: 1 1 auto; box-sizing: border-box; }

  /* Quick-action tile icon chips -- a plain pastel-tint background, one
     per action, purely custom CSS since s-box/s-icon only expose neutral
     admin grays and semantic tones (success/warning/critical), not
     arbitrary brand-safe pastels. The icon itself stays Polaris's default
     color; only the chip behind it is tinted. */
  .fp-quickaction-icon { width: 40px; height: 40px; border-radius: var(--fp-radius-card); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  .fp-qa-blue { background: #e5f0ff; }
  .fp-qa-purple { background: #f2e9ff; }
  .fp-qa-orange { background: #fff2df; }
  .fp-qa-green { background: #e3f6ec; }
  .fp-qa-teal { background: #e0f5f3; }

  /* Deterministic replacement for <s-table>: its "auto" variant chose
     list mode (labels stacked left, values disconnected far right) at
     full desktop width in this app every time it was tested live,
     despite the container being nowhere near narrow -- confirmed via
     side-by-side screenshots, not assumed. No override exists (variant
     only accepts "auto" | "list", not a way to force "table"), so rather
     than keep guessing at why "auto" misjudges this specific layout,
     this is a plain CSS grid: same row/column shape, full control, no
     hidden responsive collapse. The wrapper element each *-body id
     lives on uses display:contents so its injected cells become direct
     grid children of .fp-grid-table instead of one nested item. */
  .fp-grid-table { display: grid; background: white; border: 1px solid #e3e3e3; border-radius: var(--fp-radius-card); overflow: hidden; }
  .fp-grid-header, .fp-grid-cell {
    padding: 12px 16px; border-bottom: 1px solid #e3e3e3; display: flex; align-items: center;
  }
  /* Per-cell hover tint, not a full-row highlight -- each *-body id's
     cells are direct grid children via display:contents (see the comment
     above .fp-grid-table), with no shared per-row wrapper element to hang
     a row-level :hover off of, and the 4 tables here have 3/3/4/6 columns
     each, so a single nth-child-based "highlight every cell in this row"
     rule can't be written once and reused -- exactly the class of CSS
     trick this file has repeatedly hit and abandoned elsewhere (see
     .fp-grid-table's own history). A per-cell tint is a smaller but real
     improvement with none of that fragility. */
  .fp-grid-cell { transition: background var(--fp-motion); }
  .fp-grid-cell:hover { background: #fafafa; }
  .fp-grid-header {
    font-size: 12px; font-weight: 600; color: #6b6b6b; text-transform: uppercase; letter-spacing: 0.03em;
    background: #fafafa;
  }
  /* Per-table column tracks, in CSS instead of an inline style attribute
     specifically so the mobile media query below can override them --
     an inline style attribute always wins over a class rule regardless of
     specificity tricks, short of !important everywhere. */
  .fp-stock-notify-table { grid-template-columns: 1.6fr 1fr 1fr 1fr auto; }
  /* One column cap and one gap for every grid on the Settings page.
     Reported per-tab (Quote window, Customer documents, Email sending,
     Back in stock, Cloud printing): "none of the tile/grid caps are
     uniform", with Storefront setup named as the one to match. Each grid
     had grown its own numbers -- 240/14 here, 220/12 for field grids,
     10 for document rows -- so no two tabs lined their fields up the
     same way. These two tokens carry Storefront setup's values, and
     every settings grid below reads them instead of its own literals.
     Change the look of all five tabs by changing these two numbers. */
  :root { --fp-settings-col: 240px; --fp-settings-gap: 14px; }
  /* align-items: start so each card is its OWN height.
     
     These used to stretch to the tallest card in the row, with the
     trailing button pushed to the bottom edge. That reads well when every
     card carries a live preview and badly when they do not: a card with
     only a sentence and a button inherited the height of a neighbour with
     a preview in it, so its button sat at the bottom of a large empty
     panel. On Storefront setup that was roughly 250px of nothing inside
     the "Quote window & card buttons" card, on desktop and phone alike.
     Sizing to content leaves a ragged bottom edge across a row, which is
     the normal look for a card grid and much better than the void. */
  .fp-setup-steps {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--fp-settings-col), 1fr));
    gap: var(--fp-settings-gap); align-items: stretch;
  }
  .fp-files-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; max-height: 340px; overflow-y: auto; }
  .fp-files-tile {
    padding: 0; border: 2px solid transparent; border-radius: var(--fp-radius-control); overflow: hidden;
    background: #f0f0f0; cursor: pointer; aspect-ratio: 1; display: block; width: 100%;
  }
  .fp-files-tile:hover { border-color: var(--fp-color-brand); }
  .fp-files-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* Settings page: horizontal tab bar per the settings-page example in
     shopify.dev/docs/apps/design/app-structure -- tabs never wrap, only
     the content below changes. */
  .fp-settings-tabs {
    display: flex; gap: 4px; flex-wrap: nowrap; overflow-x: auto;
    background: white; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-card);
    padding: 4px; margin-block-end: 16px;
  }
  /* Wrap at EVERY width. Desktop clipped "Job log" behind an
     affordance-free sideways scroll (seen at 1440px); phones had the
     same scroll with the same problem, just more of it -- and the two
     widths behaving differently was itself confusing (reported
     2026-09-05). Chips that wrap are legible everywhere. */
  .fp-settings-tabs { flex-wrap: wrap; overflow-x: visible; }
  .fp-settings-tabs [role="tab"] {
    white-space: nowrap; border: none; background: transparent; cursor: pointer;
    padding: 8px 14px; border-radius: var(--fp-radius-control); font: inherit; font-size: 13.5px; font-weight: 500;
    color: var(--fp-color-subdued, #6b6b6b);
  }
  .fp-settings-tabs [role="tab"]:hover { background: #f2f2f4; color: inherit; }
  .fp-settings-tabs [role="tab"][aria-selected="true"] { background: #f2f2f4; color: inherit; font-weight: 600; }
  .fp-settings-tabs [role="tab"]:focus-visible { outline: 2px solid var(--fp-focus-ring); outline-offset: 1px; }
  .fp-settings-layout { display: block; }
  /* Uneven gaps between stacked Settings cards, reported live: .fp-block's
     own margin-block-end (used elsewhere on the page) fights whatever
     internal spacing a given <s-section> happens to carry in its own
     shadow DOM depending on its content (heading length, slotted
     children, etc.) -- the same class of "external CSS can't reliably
     override an <s-*> component's own box" issue already documented for
     border/flex-wrap/gap. A flex column with its own gap on the PARENT
     produces uniform spacing purely from the parent's layout, immune to
     whatever each child's own margin does -- so the per-card margin is
     zeroed out here rather than trying to fix it card-by-card. */
  .fp-settings-main { min-width: 0; display: flex; flex-direction: column; gap: 16px; }
  .fp-settings-main .fp-block { margin-block-end: 0; }
  .fp-settings-main .fp-block[data-settings-hidden] { display: none; }
  /* Live install-state chips on the storefront setup cards. */
  /* Button + install-state chip on one aligned row at the bottom of each
     setup step (item 23) -- chips used to trail the headings, which made
     every card's columns ragged. */
  .fp-setup-step-actions { display: flex; align-items: center; gap: 10px; }
  .fp-status-chip { display: inline-block; margin-left: 6px; padding: 1px 9px; border-radius: 999px; font-size: 11px; font-weight: 600; vertical-align: middle; }
  .fp-status-chip:empty { display: none; }
  .fp-status-chip[data-state="on"] { background: #d3f2e3; color: #00543d; }
  .fp-status-chip[data-state="off"] { background: #f2f2f4; color: #6b6b6b; }
  .fp-status-chip[data-state="unknown"] { background: #fff1d6; color: #7a5a10; }

  /* Quote widget settings editor (Quotes page). Plain grid/flex, not
     s-grid/s-stack, per the Polaris shadow-DOM gotcha at the top of this
     file. */
  .fp-qs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--fp-settings-col), 1fr)); gap: var(--fp-settings-gap); align-items: end; }
  /* Live preview of the storefront quote window -- a scoped, static copy
     of the widget's own modal styling (quote-widget.css), kept visually
     in sync by hand. Non-interactive by design (aria-hidden, pointer-
     events none) -- it's a picture of the config, not a working form. */
  .fp-qsp { pointer-events: none; background: #ececec; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); padding: 22px; }
  .fp-qsp-window { max-width: 700px; margin: 0 auto; background: #fff; box-shadow: 0 12px 32px rgba(0,0,0,0.25); overflow: hidden; }
  .fp-qsp-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #e7e7e7; font-weight: 600; }
  .fp-qsp-head span:last-child { color: #6b6b6b; font-size: 18px; }
  .fp-qsp-body { display: grid; grid-template-columns: 1fr 1fr; }
  .fp-qsp-col { padding: 14px 16px; }
  .fp-qsp-col-title { font-weight: 600; font-size: 12.5px; margin-bottom: 10px; }
  .fp-qsp-col-details { border-right: 1px solid #e7e7e7; }
  .fp-qsp-col-items { background: rgba(0,0,0,0.025); }
  .fp-qsp-label { display: block; font-size: 11.5px; font-weight: 500; margin-bottom: 8px; }
  .fp-qsp-col-details > .fp-qsp-input, .fp-qsp-address > .fp-qsp-input { margin-bottom: 8px; }
  /* Slightly darker than the #d82c0d used for critical banners: this
     marker sits on the preview's own #ececec panel, where that red
     measures 4.15:1, under AA. At #c62b0d it clears 4.5 on every
     surface it can land on. */
  .fp-qsp-req { color: #c62b0d; font-weight: 700; }
  /* Placeholder text sits inside the box in the preview, the way it will
     on the storefront, so a merchant can judge whether their wording
     actually fits the field. */
  /* One box per field with its title inside it, mirroring the
     storefront's floating-label fields. */
  .fp-qsp-input {
    min-height: 34px; border: 1px solid #d0d0d0; border-radius: var(--fp-radius-small); background: #fff;
    padding: 6px 8px; font-size: 11px; font-weight: 400; color: var(--fp-color-subdued);
    display: flex; flex-direction: column; justify-content: center; gap: 2px; overflow: hidden;
  }
  .fp-qsp-fieldtitle { display: block; font-size: 10px; border-radius: 3px; padding: 0 2px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .fp-qsp-area { height: 48px; }
  .fp-qsp-submit { margin-top: 4px; padding: 9px; text-align: center; font-weight: 600; font-size: 12.5px; }
  .fp-qsp-item { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; font-size: 12px; }
  .fp-qsp-thumb { width: 34px; height: 34px; border-radius: var(--fp-radius-small); background: #e2e2e2; border: 1px solid #d5d5d5; flex: none; }
  .fp-qsp-item-title { flex: 1; }
  .fp-qsp-qty { width: 38px; height: 24px; border: 1px solid #d0d0d0; border-radius: var(--fp-radius-small); background: #fff; flex: none; }
  @media (max-width: 640px) { .fp-qsp-body { grid-template-columns: 1fr; } .fp-qsp-col-details { border-right: none; border-bottom: 1px solid #e7e7e7; } }
  .fp-qs-fields { display: flex; flex-direction: column; gap: var(--fp-settings-gap); }
  /* Quote window's field rows and Customer documents' document rows are
     the same shape -- [main field][second field][Remove] -- and were the
     most visible half of the "no two settings tabs line up" report: they
     had different column ratios (2fr 1fr auto vs 2fr 2fr auto) AND
     different gaps (10px vs 14px), so switching between the two tabs
     moved every control sideways. One rule for both now. */
  .fp-qs-field-row, .fp-qd-row {
    /* The third track is a fixed width, not auto. With auto it sizes to
       whatever that row's button says -- "Remove" vs "Add domain" vs
       "Save" -- which pushes the 2fr/1fr fields to different widths on
       every row, so no two rows lined up even within one card. A fixed
       action column makes the field edges identical everywhere. */
    display: grid; grid-template-columns: 2fr 1fr var(--fp-settings-action, 118px);
    gap: var(--fp-settings-gap); align-items: end;
  }
  .fp-qs-field-spacer { min-width: 74px; }
  @media (max-width: 640px) {
    .fp-qs-field-row, .fp-qd-row { grid-template-columns: 1fr 1fr; }
    .fp-qs-field-spacer { display: none; }
  }
  /* Form builder. One collapsed row per field, expanded on demand: a
     merchant scanning "what does my form ask for" reads a list of labels,
     and only the field they are editing opens up. A native <details> is
     the whole mechanism, so it is keyboard-operable without any JS. */
  .fp-qsb-field {
    border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); background: #fff;
  }
  .fp-qsb-summary {
    display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
    padding: 12px 14px; cursor: pointer; list-style: none;
  }
  .fp-qsb-summary::-webkit-details-marker { display: none; }
  /* The default marker is replaced rather than just removed: a row with
     no affordance at all does not read as openable (measured in a real
     render -- the rows looked like static list items). */
  .fp-qsb-summary::before {
    content: "\203a"; display: inline-block; width: 10px; flex: none;
    color: var(--fp-color-subdued); transition: transform 120ms ease;
  }
  .fp-qsb-field[open] > .fp-qsb-summary::before { transform: rotate(90deg); }
  @media (prefers-reduced-motion: reduce) { .fp-qsb-summary::before { transition: none; } }
  .fp-qsb-field[open] .fp-qsb-summary { border-bottom: 1px solid var(--fp-color-border); }
  .fp-qsb-summary:focus-visible { outline: 2px solid var(--fp-color-focus); outline-offset: -2px; }
  .fp-qsb-name { font-weight: 600; }
  .fp-qsb-meta { color: var(--fp-color-subdued); font-size: 12.5px; }
  .fp-qsb-body { padding: 14px; display: flex; flex-direction: column; gap: var(--fp-settings-gap); }
  .fp-qsb-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--fp-settings-gap); }
  .fp-qsb-add { display: grid; grid-template-columns: minmax(0, 1fr) var(--fp-settings-action, 118px); gap: var(--fp-settings-gap); align-items: end; }
  @media (max-width: 640px) {
    .fp-qsb-grid { grid-template-columns: 1fr; }
    .fp-qsb-add { grid-template-columns: 1fr; }
  }
  .fp-qsp-help { display: block; color: var(--fp-color-subdued); font-size: 10.5px; margin-top: 3px; }
  /* An address previews as the group it really is, so a merchant can see
     how much of the form it occupies before a customer does. No panel
     around it: on the storefront the lines sit in the form's own flow
     under a subtitle, and a bordered box here would preview a separate
     section that does not exist. */
  .fp-qsp-address {
    margin-bottom: 8px;
    display: flex; flex-direction: column; gap: 5px;
  }
  .fp-qsp-addr-legend { font-size: 11.5px; font-weight: 600; }
  .fp-qsp-addr-row { display: grid; grid-template-columns: 1fr 1fr 1.2fr; gap: 5px; }
  .fp-qsp-check { display: flex; align-items: center; gap: 7px; }
  .fp-qsp-box { width: 13px; height: 13px; border: 1px solid #c9c9c9; background: #fff; flex: none; }
  .fp-qsp-select { display: flex; align-items: center; }
  .fp-qsp-select::after { content: "\25be"; margin-left: auto; color: #8a8a8a; }
  .fp-setup-step {
    display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
    background: #f6f6f7; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); padding: 14px;
  }
  .fp-setup-step-desc { width: 100%; }
  /* flex:1 on the space between the description and the trailing button
     means that button always sits flush with the card's bottom edge
     (the grid already stretches every .fp-setup-step to the row's
     tallest card -- see .fp-setup-steps) instead of trailing right after
     whatever content happens to be above it. */
  .fp-setup-step-fill { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 6px; width: 100%; }
  /* Same .fp-preview/.fp-preview-label/.fp-qsp treatment the Back in
     stock appearance card's own live preview already uses (reported
     live: the two should look uniform, not two different preview
     styles on the same Settings page) -- these two rules only resize
     that shared box down for a single small button instead of a full
     drawer mockup. */
  /* This was nowrap, to stop <s-stack direction="inline"> stacking a row's
     two buttons vertically when the old grid table's "auto" column got
     squeezed on a narrow screen. That grid is gone, and under s-table's
     mobile LIST layout nowrap does something worse than the problem it
     solved: the actions sit on one line and the ones past the container
     edge are simply clipped away. Confirmed live at 390px on the templates
     list, where Edit, Duplicate and Delete were all unreachable.
     Wrapping is correct here -- every action stays reachable, and on a
     desktop-width cell there is room enough that it never triggers. */
  .fp-job-actions { justify-content: flex-end; }
  /* Name + its badges on ONE line -- unwrapped, each badge fell onto its
     own row and every badged template row grew taller than its
     neighbours (seen live 2026-09-05). */
  .fp-name-with-badges { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  /* Heading + dismiss stay side by side at every width -- the generic
     card-row stacks below 640px, which dropped this X onto its own line. */
  .fp-guide-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
  .fp-qp-row {
    display: grid; grid-template-columns: auto minmax(120px, 1.2fr) 1fr 1fr 1fr auto auto;
    gap: 8px; align-items: center; padding: 8px 10px;
    border-bottom: 1px solid var(--fp-color-border, rgba(0,0,0,0.08));
  }
  .fp-qp-row:last-child { border-bottom: none; }
  .fp-qp-arrows { display: flex; flex-direction: column; gap: 2px; }
  .fp-qp-arrow {
    background: none; border: 1px solid rgba(0,0,0,0.15); border-radius: 4px;
    width: 28px; height: 24px; line-height: 1; font-size: 11px; cursor: pointer; padding: 0;
    color: inherit;
  }
  .fp-qp-arrow:hover:not(:disabled) { border-color: var(--fp-color-brand); color: var(--fp-color-brand); }
  .fp-qp-arrow:focus-visible { outline: 2px solid var(--fp-focus-ring); outline-offset: 1px; }
  .fp-qp-arrow:disabled { opacity: 0.3; cursor: default; }
  .fp-qp-name { font-weight: 600; }
  .fp-qp-addrow { display: flex; gap: 8px; align-items: end; }
  @media (max-width: 720px) {
    /* Stack: identity row on top, then each select full-width. The
       desktop 7-column line crushed the selects to slivers at 390px
       (seen in a real phone screenshot pass, 2026-09-01). */
    .fp-qp-row {
      grid-template-columns: auto 1fr auto;
      grid-template-areas: "arrows name remove" "worker worker worker" "printer printer printer" "save save save";
    }
    .fp-qp-row .fp-qp-arrows { grid-area: arrows; flex-direction: row; }
    .fp-qp-row .fp-qp-name { grid-area: name; }
    .fp-qp-row .fp-qp-cell-worker { grid-area: worker; }
    .fp-qp-row .fp-qp-cell-printer { grid-area: printer; }
    .fp-qp-row .fp-qp-cell-save { grid-area: save; }
    .fp-qp-row .fp-qp-remove { grid-area: remove; }
  }
  .fp-row-actions { display: flex; flex-wrap: wrap; gap: 6px; }
  /* Inside a table cell the actions hug the ROW END (user review: the
     visual gap belongs left of Edit, not trailing Delete). */
  s-table-cell .fp-row-actions { justify-content: flex-end; }
  s-table-cell .fp-qp-remove { display: flex; justify-content: flex-end; }
  /* On a phone the actions column is narrow enough that each button
     lands on its own line, turning every template into a tall ladder
     (reported live from the Shopify mobile app). Shrink them and let
     several share a row instead. */
  /* Phones: show the two everyday actions plus a More toggle; the rest
     appear on demand. A horizontal-scroll strip was tried first and is
     WRONG here -- the scroll is taken by the table ancestor, so the
     whole card slides sideways (screenshot-confirmed). display on the
     host element is reachable (unlike a component's internals), which
     is what makes this work at all. */
  .fp-row-actions [data-more] { display: none; }
  @media (max-width: 640px) {
    .fp-row-actions [data-more] { display: inline-flex; }
    .fp-row-actions [data-mobile-extra] { display: none; }
    .fp-row-actions.fp-actions-open [data-mobile-extra] { display: inline-flex; }
  }
  /* Wrapping sibling of .fp-row-actions, for the trailing action group of a
     full-width card rather than a table row. Two buttons in the third column
     of an auto/1fr/auto grid do not fit a phone, and nowrap there pushes
     them off the card instead of stacking them. */
  .fp-card-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }

  /* DNS records, styled on Cloudflare's DNS table by explicit request:
     plain monospace on quiet rows, generous rhythm, uppercase column
     labels, status on the right. Values are TEXT, not bordered chips --
     a wall of boxed inputs was the reported problem. */
  /* Flow-style code input (items 16/17). Gutter and textarea share one
     line-height so the numbers can never drift from the lines; the gutter
     scroll-follows the textarea. */
  .fp-code-editor { border: 1px solid #8c9196; border-radius: 8px; overflow: hidden; background: #fff; }
  .fp-code-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-bottom: 1px solid #e3e4e6; background: #f7f7f8; }
  .fp-code-title { font-weight: 600; font-size: 13px; color: #303030; }
  .fp-code-badge {
    font-size: 11px; font-weight: 600; padding: 1px 8px; border-radius: 999px;
    background: #e4e5e7; color: #4a4a4a;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  }
  .fp-code-meta { margin-left: auto; font-size: 12px; color: var(--fp-color-subdued); font-variant-numeric: tabular-nums; }
  .fp-code-area { display: flex; align-items: stretch; }
  .fp-code-gutter {
    margin: 0; padding: 10px 8px 10px 12px; text-align: right; user-select: none;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px; line-height: 20px; color: #5f6368; background: #fafbfb;
    border-right: 1px solid #e3e4e6; overflow: hidden; min-width: 34px;
  }
  .fp-code-gutter .fp-code-cur { color: #1a1c1e; font-weight: 700; }
  .fp-code-input {
    flex: 1; border: 0; outline: none; resize: none; padding: 10px 12px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px; line-height: 20px; color: #1a1c1e; background: #fff;
    white-space: pre; overflow: auto; min-height: 330px; box-sizing: border-box;
  }
  .fp-code-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

  /* Boxed DNS record index (item 12): one record, presented as labeled
     read-only boxes with real Copy buttons, not a table. */
  .fp-dns-index { display: flex; flex-direction: column; gap: 14px; margin-block-start: 10px; }
  .fp-dns-rec { display: flex; flex-direction: column; gap: 8px; }
  .fp-dns-rec-head { display: flex; align-items: center; gap: 10px; }
  .fp-dns-checkrow { display: flex; margin-block-start: 4px; }
  .fp-dns-boxrow { display: flex; align-items: center; gap: 8px; }
  .fp-dns-boxlabel { flex: 0 0 88px; font-size: 12px; color: var(--fp-color-subdued); }
  .fp-dns-box {
    flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 8px;
    border: 1px solid #d9dbdd; border-radius: 8px; background: #fafbfb;
    padding: 6px 10px;
  }
  .fp-dns-scroll { overflow-x: auto; margin-block-start: 10px; }
  .fp-dns-table {
    width: 100%; border-collapse: collapse;
    /* Fixed layout is what makes the one-line clamp work at all: with
       auto layout a nowrap value sets the column's intrinsic width to
       the FULL string, the table blows out sideways, and the host
       column gets crushed to one character per line. */
    table-layout: fixed;
    border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control);
    overflow: hidden; background: white;
  }
  /* In fixed layout the FIRST row's cells set every column width, so
     all four are pinned on the header cells: leaving one out hands its
     slack to a neighbour and starves the value column, which is the one
     that needs the room. */
  .fp-dns-table th:nth-child(1) { width: 52px; }
  .fp-dns-table th:nth-child(2) { width: 30%; }
  .fp-dns-table th:nth-child(4) { width: 128px; }
  .fp-dns-table th {
    text-align: left; padding: 10px 16px;
    font-size: 10.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
    color: #5c5f62; background: #fafbfb;
    border-block-end: 1px solid var(--fp-color-border);
  }
  .fp-dns-table td {
    text-align: left; vertical-align: top; padding: 14px 16px;
    border-block-end: 1px solid #f0f0f0; font-size: 12.5px;
  }
  .fp-dns-table tbody tr:last-child td { border-block-end: none; }
  .fp-dns-table tbody tr:hover td { background: #fafbfb; }
  .fp-dns-type-cell { width: 56px; font-weight: 600; font-size: 12px; color: #303030; }
  .fp-dns-status-cell { width: 110px; text-align: right; }
  /* Text plus its actions on one line; the text takes the slack. */
  .fp-dns-cell { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
  .fp-dns-mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px; color: #1a1c1e; word-break: break-all; white-space: normal;
  }
  /* One line with disclosed truncation until Show is pressed. */
  .fp-dns-clamped {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    display: block; flex: 1;
  }
  .fp-dns-open { word-break: break-all; }
  .fp-dns-sublabel {
    font-size: 10.5px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
    color: #5c5f62; margin-block-end: 3px;
  }
  /* Ghost actions: quiet until pointed at, always visible, always
     focusable -- never hover-revealed. */
  .fp-dns-ghost, .fp-dns-copy {
    font: inherit; font-size: 11.5px; font-weight: 500; cursor: pointer; white-space: nowrap;
    padding: 2px 6px; background: none; color: var(--fp-color-brand);
    border: 1px solid transparent; border-radius: var(--fp-radius-small);
    flex: 0 0 auto;
  }
  .fp-dns-ghost:hover, .fp-dns-copy:hover { background: rgba(0,110,82,0.08); }
  .fp-dns-ghost:focus-visible, .fp-dns-copy:focus-visible { outline: 2px solid var(--fp-color-focus); outline-offset: 1px; }

  /* ------------------------------------------------------------------
     Email sending panel structure. Every rule below was collateral
     damage of the DNS-table restyle: the excision that replaced the old
     table CSS also swallowed the identity grid, the address table, the
     records disclosure and the section headings, which left the whole
     panel as an unstyled pile of full-width fields. If a class appears
     in renderEmailDomain/renderEmailAddresses, its rule lives HERE. */

  /* Header row: identity left, actions right, anchored by a divider so
     the sections below read as belonging to this domain. */
  .fp-domain-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; flex-wrap: wrap;
    padding-block-end: 12px; border-block-end: 1px solid #f0f0f0;
  }
  .fp-domain-head-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
  .fp-domain-head-actions { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }

  /* Section headings inside the card. Each group is separated by a
     hairline and consistent air, which is most of what "finished" is. */
  .fp-sub-heading {
    font-size: 13px; font-weight: 600; margin: 0 0 2px;
    color: var(--fp-color-text);
  }

  /* Records disclosure: a real panel inside the card, its own surface,
     wrapping the table when open. */
  .fp-dns-details {
    margin-block-start: 12px;
    background: #f6f6f7;
    border: 1px solid var(--fp-color-border);
    border-radius: var(--fp-radius-control);
    overflow: hidden;
  }
  .fp-dns-details > summary {
    cursor: pointer; font-size: 12.5px; font-weight: 600;
    padding: 10px 12px; color: var(--fp-color-text);
    display: flex; align-items: center; gap: 6px; user-select: none;
  }
  .fp-dns-details[open] > summary { border-block-end: 1px solid var(--fp-color-border); background: #f0f0f1; }
  .fp-dns-details > summary:hover { background: #f0f0f1; }
  .fp-dns-details > summary:focus-visible { outline: 2px solid var(--fp-color-focus); outline-offset: -2px; }
  .fp-dns-details > *:not(summary) { padding-inline: 12px; }
  /* SPF guidance banner. Standalone form has its own corners; the flush
     form full-bleeds under the details header with the top corners
     squared, per live review ("so it looks flush"). */
  .fp-dns-banner { display: flex; align-items: flex-start; gap: 10px; background: #ffffff; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); padding: 12px 14px; margin-block-start: 8px; font-size: 13px; color: var(--fp-color-text); line-height: 1.45; }
  .fp-dns-details > .fp-dns-banner-flush { padding-inline: 14px; margin: 0; border-inline: 0; border-block-start: 0; border-radius: 0 0 var(--fp-radius-control) var(--fp-radius-control); }
  .fp-dns-banner-icon { flex: none; inline-size: 20px; block-size: 20px; border-radius: 6px; display: grid; place-items: center; color: #ffffff; font-size: 12px; font-weight: 700; margin-block-start: 1px; }
  .fp-dns-banner-success .fp-dns-banner-icon { background: var(--fp-color-success); }
  .fp-dns-banner-warning .fp-dns-banner-icon { background: #b98900; }
  .fp-dns-details > *:not(summary):last-child { padding-block-end: 12px; }

  /* Sender identity: two fields side by side, save hidden (the Shopify
     save bar drives it). */
  .fp-email-identity {
    margin-block-start: 20px; padding-block-start: 16px;
    border-block-start: 1px solid #f0f0f0;
  }
  .fp-email-identity-row {
    display: grid; grid-template-columns: 1fr 1fr auto;
    gap: 10px; align-items: end; margin-block-end: 6px;
  }
  @media (max-width: 640px) { .fp-email-identity-row { grid-template-columns: 1fr; align-items: stretch; } }
  .fp-email-suffix { font-size: 13px; color: #6b6b6b; padding-block-end: 8px; align-self: end; white-space: nowrap; }
  .fp-test-to-row { display: flex; gap: 8px; align-items: end; }
  .fp-test-to-row > s-text-field { flex: 1 1 auto; min-width: 0; }

  /* Sending addresses: the same table language as the DNS records. */
  .fp-addr-block {
    margin-block-start: 20px; padding-block-start: 16px;
    border-block-start: 1px solid #f0f0f0;
  }
  /* Captions under the group headings: quiet, bounded measure, real
     margin below so the content does not sit on top of them. */
  .fp-email-identity s-paragraph, .fp-addr-block > s-paragraph {
    display: block; max-inline-size: 70ch; margin-block-end: 10px;
  }
  .fp-addr-table {
    width: 100%; border-collapse: collapse; table-layout: fixed;
    border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control);
    overflow: hidden; background: white;
  }
  .fp-addr-table th {
    text-align: left; padding: 10px 16px;
    font-size: 10.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
    color: #5c5f62; background: #fafbfb;
    border-block-end: 1px solid var(--fp-color-border);
  }
  .fp-addr-table th:nth-child(1) { width: 40%; }
  .fp-addr-table th:nth-child(3) { width: 96px; }
  .fp-addr-table td {
    padding: 10px 16px; vertical-align: middle;
    border-block-end: 1px solid #f0f0f0; font-size: 12.5px;
  }
  .fp-addr-table tbody tr:last-child td { border-block-end: none; }
  .fp-addr-table tbody tr:hover td { background: #fafbfb; }
  .fp-addr-domain {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px; color: #5c5f62; white-space: nowrap;
  }
  .fp-addr-actions { text-align: right; white-space: nowrap; }
  .fp-addr-add { display: flex; align-items: center; gap: 10px; margin-block-start: 12px; }
  .fp-addr-counter {
    font-size: 11.5px; font-weight: 500; color: #5c5f62;
    background: #f6f6f7; border-radius: 999px; padding: 2px 10px;
  }

  @media (max-width: 640px) {
    /* Below this the four columns cannot hold a 400 character value, so
       the table keeps its shape and scrolls rather than collapsing. */
    .fp-dns-table { table-layout: auto; min-width: 560px; }
  }


  /* Job-log toolbar (Settings > Job Log): column picker + search grow +
     purge. The selects/fields are wrapped in plain divs because the
     s-* hosts are display:contents -- flex sizing on them is ignored. */
  .fp-jl-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  .fp-joblog-purge-row { display: flex; justify-content: flex-end; }

  /* Icon + copy + actions rows (the trust footnote and Help and support
     cards, and the two header rows above them).

     These were <s-grid gridTemplateColumns="auto 1fr auto">, which holds
     three fixed columns at EVERY width. On a phone that crushed the copy
     column to about a third of the screen -- "Every document is rendered"
     wrapped one word per line beside a mostly empty actions column. A
     Polaris component's own layout cannot be overridden from outside
     (the shadow-DOM note at the top of this file), so this is a plain
     CSS grid, the same fix already applied to every other squeezed
     s-grid here.

     Below 640px the actions drop to their own full-width row under the
     copy instead of competing with it for horizontal space. */
  .fp-card-row {
    display: grid; grid-template-columns: auto 1fr auto;
    align-items: center; gap: 12px;
  }
  .fp-card-row-2 { grid-template-columns: 1fr auto; }
  @media (max-width: 640px) {
    .fp-card-row { grid-template-columns: auto 1fr; }
    .fp-card-row-2 { grid-template-columns: 1fr; }
    .fp-card-row .fp-card-actions {
      grid-column: 1 / -1; justify-content: flex-start; margin-block-start: 4px;
    }
  }
  /* Setup guide steps. Plain elements with real CSS rather than nested
     s-box, per the shadow-DOM note above -- these need a hover state, a
     focus ring and an exact grid, none of which survive being set from
     outside a Polaris component. Colours come from the same tokens the
     rest of the app uses so it still reads as admin chrome. */
  .fp-setup-item { border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); margin-block-start: 8px; background: white; }
  .fp-setup-item-done { background: #f6f6f7; }
  .fp-setup-item-head {
    display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px;
    padding: 14px 16px; cursor: pointer; border-radius: var(--fp-radius-control);
  }
  .fp-setup-item-head:hover { background: rgba(0,0,0,0.03); }
  .fp-setup-item-head:focus-visible { outline: 2px solid var(--fp-color-focus, #005bd3); outline-offset: -2px; }
  .fp-setup-item-done .fp-setup-item-head { cursor: default; }
  .fp-setup-item-done .fp-setup-item-head:hover { background: transparent; }
  .fp-setup-item-body { padding: 0 16px 16px 44px; display: grid; gap: 12px; justify-items: start; }
  @media (max-width: 640px) { .fp-setup-item-body { padding-inline-start: 16px; } }
  /* Platform chooser rows. Native anchors, not s-button: these are real
     downloads, so the browser download attribute does the work and the row
     can carry a second line of explanatory text a button cannot. */
  .fp-download-choices { display: grid; gap: 10px; }
  .fp-download-choice { display: block; padding: 14px 16px; border: 1px solid var(--fp-color-border);
    border-radius: var(--fp-radius-card); background: white; text-decoration: none; color: inherit; }
  .fp-download-choice:hover { border-color: #8c9196; background: #fafbfb; }
  .fp-download-os { display: block; font-weight: 600; font-size: 14px; }
  .fp-download-note { display: block; margin-top: 2px; font-size: 12.5px; color: var(--fp-color-subdued); }

  /* Loading placeholders for list surfaces: cold loads (session token
     exchange + isolate cold start) can take several seconds, and a bare
     table with only headers reads as broken (misread twice during the
     2026-08-17 live pass). Each loader overwrites its container's
     innerHTML when data or an error arrives, so these self-clear. */
  .fp-loading { color: var(--fp-color-subdued, #6b6b6b); font-size: 13px; padding: 14px 16px; display: block; }
  .fp-loading-row { grid-column: 1 / -1; }

  /* "Start from a template" starter gallery (Templates page). Thumbnails
     are sandboxed srcdoc iframes of the starter's sample-data preview,
     scaled down from full A4 by JS (transform, measured per card). */
  .fp-starter-gallery { margin-top: 12px; }
  .fp-starter-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }
  .fp-starter-card {
    background: white; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-card);
    padding: 12px; display: flex; flex-direction: column; gap: 8px;
  }
  .fp-starter-thumb {
    width: 100%; aspect-ratio: 210 / 297; overflow: hidden; position: relative;
    border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); background: white;
  }
  .fp-starter-thumb iframe {
    width: 840px; height: 1188px; border: 0; pointer-events: none;
    transform-origin: top left; position: absolute; top: 0; left: 0;
  }
  .fp-starter-name { font-weight: 600; }
  .fp-starter-desc { color: var(--fp-color-subdued, #616161); font-size: 12px; min-height: 32px; }
  /* Bottom-align the Use button across cards whose descriptions wrap to
     different line counts, so a row of cards reads as one aligned unit. */
  .fp-starter-card s-button { margin-top: auto; align-self: start; }

  /* Workers/Templates/Job log now each live in one section (heading,
     intro, actions, AND the table together) instead of a separate intro
     card + separate table card -- stacking two cards for one resource
     read as visually broken. This just gives the table a bit of breathing
     room from whatever content sits above it in the same card. */
  /* No own margin: the card's base stack supplies the 16px rhythm now.
     The old 12px here stacked on top of it (measured 24-30px gaps). */
  .fp-inset-table { margin-block-start: 0; }
  /* #workflows-grid carries .fp-inset-table too (same 12px value, same
     class) and the DOM structure above it is identical to Templates' --
     paragraph, button, this element, nothing else -- so no override is
     needed here; see .fp-workflow-card below for why an earlier attempt
     to compensate with a margin fudge-factor was the wrong fix and has
     been removed. */

  /* Workers/Email automations each have an #x-onboarding div sitting
     between their "Add" button and their table, populated with a banner
     only in the empty-state case (e.g. "no workers paired yet") and left
     as innerHTML = "" otherwise. Reported live: the gap above the Workers
     table was visibly larger than Templates' or Job log's identical-
     looking button-to-table gap, even though .fp-inset-table's margin is
     the same everywhere -- Templates has no such placeholder div. The
     section's own automatic inter-child spacing doesn't know or care
     that a sibling is visually empty; it still allocates a full gap on
     both sides of it, doubling the effective space between the button
     and the table whenever the onboarding banner isn't showing. :empty
     only matches when a div truly has zero child nodes (exactly what
     innerHTML = "" produces), so this only removes the phantom gap in
     the no-banner case -- an actual onboarding banner still displays and
     spaces normally. (Workflows had the identical setup and doesn't
     anymore -- its onboarding banner turned out to still measurably
     affect spacing even at display:none, so it was removed as a separate
     sibling entirely rather than just hidden; see loadWorkflows'
     own comment.) */
  #workers-onboarding:empty { display: none; }
  /* Workflows kept reading as having a bigger button-to-list gap than
     Templates'/Workers' identical-looking one even after two rounds of
     fixes (the <s-stack> replacement, then the <s-grid> replacement) --
     each closed part of the gap but never quite matched exactly, because
     Workflows was still structurally different: its button/onboarding/
     grid lived inside a wrapper (first <s-stack>, then .fp-block-stack)
     with its own flex gap, on top of whatever <s-section>'s own
     automatic inter-child spacing already provides -- two spacing
     sources stacking, approximating but never exactly equalling
     Templates' one source (section spacing + .fp-inset-table's 12px).
     Rather than keep tuning a second gap value to match, removed the
     wrapper entirely: button/onboarding-div/grid are now direct
     <s-section> children exactly like Workers/Templates, and
     #workflows-grid carries .fp-inset-table directly (the exact same
     class, not an equivalent value) so there is structurally nothing
     left that could differ. */
  .fp-workflow-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
  .fp-workflow-cell { display: flex; }
  /* Plain CSS border/background/padding instead of the <s-clickable>
     border="base"/borderRadius="base"/padding="base"/background="base"
     props previously used here -- same fix already applied to
     .fp-plan-card and .fp-quickaction-row elsewhere in this file. Those
     props draw their box on an element INSIDE the component's shadow
     root, not on the light-DOM host itself (confirmed the hard way: an
     earlier attempt to add an external CSS border on top of a working
     border="base" prop elsewhere in this file produced a visible double
     border, because the external border became a second box nested
     around the shadow-drawn one rather than replacing it). That shadow-
     root inset is also the real explanation for a gap this card's own
     button-to-grid spacing kept measuring slightly smaller than
     Templates'/Email automations' identical-looking one even once the
     surrounding DOM structure was made identical -- a previous fix
     compensated with a guessed margin-block-start override on
     #workflows-grid instead of addressing the actual mismatched box, and
     that override has been removed now that this card draws its own
     border the same plain-CSS way .fp-grid-table does (matching values:
     1px solid #e3e3e3, 8px radius, white background, 16px padding, the
     same "base" spacing token .fp-plan-card already uses). */
  .fp-workflow-card {
    display: block; flex: 1 1 auto; box-sizing: border-box; cursor: pointer;
    border: 1px solid #e3e3e3; border-radius: var(--fp-radius-card); background: white; padding: 16px;
  }

  /* Rows, not one big flex-wrap bag -- name+trigger on their own row,
     Enabled on its own row, step-add buttons on their own row, always,
     on every screen size. This used to be a single flex-wrap row with
     Enabled given flex-basis:100% only under a mobile media query, which
     was reported live as still not breaking onto its own line on a real
     phone -- rather than keep guessing at why that one rule wasn't
     taking effect inside a modal (a real possibility: components
     rendered through App Bridge's <s-modal> may not resolve media
     queries the same way page content does), removing the conditional
     entirely and making the row split unconditional and structural
     guarantees the fix regardless of viewport-detection specifics. The
     visual cost is one extra row on desktop too, which reads as
     perfectly normal (a toggle above its actions), not a regression. */
  .fp-canvas-toolbar { display: flex; flex-direction: column; gap: 14px; }
  .fp-canvas-toolbar-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; }
  /* Plain div, not flex/min-width applied directly to <s-text-field>/
     <s-select> -- confirmed live (screenshot review, 2026-08-13) that a
     Polaris component ignores flex-basis/min-width set on its own host
     element and just claims a full row each regardless, same class of
     "the prop/style exposed on the light-DOM host doesn't control the
     component's real internal box" issue this file has hit and fixed
     repeatedly elsewhere (.fp-quickaction-cell wrapping <s-clickable>,
     .fp-row-actions wrapping button pairs). Wrapping in an ordinary div
     and sizing THAT instead sidesteps it the same way. */
  .fp-toolbar-field { flex: 1 1 220px; min-width: 220px; }
  .fp-toolbar-field-narrow { flex-basis: 140px; min-width: 140px; max-width: 180px; }
  .fp-toolbar-field s-text-field, .fp-toolbar-field s-select { width: 100%; }
  .fp-canvas-toolbar-row s-checkbox { padding-block-end: 2px; }

  /* Vertical linear flow (2026-08-13 rebuild, replacing the free-drag node
     canvas -- see the top-of-file comment for why). Modeled on Shopify
     Flow's own editor: a straight top-to-bottom chain of cards connected
     by a plain line, a condition card forking into two labeled branch
     columns. Deliberately normal document flow (flexbox, no absolute
     positioning, no SVG line math, no drag) -- every interactive element
     is a real <button>, so this is fully keyboard-operable without the
     drag-only accessibility gap the old canvas knowingly shipped with. */
  /* ---------- Flow-style canvas (review rebuild) ---------- */
  .fp-canvas-viewport {
    position: relative; height: 620px; overflow: hidden; border-radius: var(--fp-radius-card);
    border: 1px solid #e1e3e5; cursor: grab; touch-action: none;
    background-color: #fafbfb;
    background-image: radial-gradient(#d8dadd 1px, transparent 1px);
    background-size: 20px 20px;
  }
  .fp-canvas-viewport:active { cursor: grabbing; }
  .fp-canvas-world { position: absolute; top: 0; left: 0; will-change: transform; }
  .fp-cv-edges { position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible; }
  .fp-cv-edges path { stroke: #8a8f98; stroke-width: 1.5; fill: none; }
  /* Native admin card, not a colored banner: white, quiet border, icon
     chip + text -- the same anatomy Flow's own cards use. */
  .fp-flow-card {
    position: absolute; width: 236px; box-sizing: border-box;
    background: white; border: 1px solid #e1e3e5; border-radius: var(--fp-radius-control);
    box-shadow: 0 1px 2px rgba(0,0,0,0.06);
    padding: 10px 12px; text-align: left; user-select: none;
  }
  .fp-flow-card.fp-flow-clickable { cursor: grab; transition: border-color var(--fp-motion), box-shadow var(--fp-motion); }
  .fp-flow-card.fp-flow-clickable:hover { border-color: #b5b8bc; box-shadow: 0 2px 8px rgba(0,0,0,0.10); }
  .fp-cv-node { z-index: 1; }
  .fp-cv-trigger { border-left: 3px solid #006e52; }
  .fp-cv-main { display: flex; align-items: center; gap: 10px; min-width: 0; }
  .fp-cv-chip {
    width: 32px; height: 32px; border-radius: var(--fp-radius-small); flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
  }
  .fp-cv-chip-trigger { background: #d5efe4; color: #045e3f; }
  .fp-cv-chip-print { background: #e0e9ed; color: #33566b; }
  .fp-cv-chip-email { background: #ddebfa; color: #1f5aa8; }
  .fp-cv-chip-webhook { background: #ece3f0; color: #5c3f68; }
  .fp-cv-chip-condition { background: #f7ecd4; color: #7a5a10; }
  .fp-cv-chip-split { background: #d9efed; color: #16645e; }
  .fp-cv-text { display: flex; flex-direction: column; min-width: 0; }
  .fp-cv-overline { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fp-color-subdued); }
  .fp-cv-title { font-size: 13px; font-weight: 600; color: #1a1c1e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .fp-cv-sub { font-size: 11.5px; color: var(--fp-color-subdued); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .fp-cv-add { position: absolute; transform: translate(-50%, -50%); }
  .fp-cv-add .fp-flow-type-menu {
    background: white; border: 1px solid #e1e3e5; border-radius: var(--fp-radius-control);
    box-shadow: 0 4px 16px rgba(0,0,0,0.12); padding: 10px; width: 300px;
  }
  .fp-cv-pill {
    position: absolute; transform: translate(-50%, -50%);
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
    padding: 3px 10px; border-radius: 12px; color: white; white-space: nowrap;
  }
  .fp-cv-pill-true { background: #006e52; }
  .fp-cv-pill-false { background: #6b6b6b; }
  .fp-cv-pill-split { background: #1c7a72; }
  .fp-flow-card-remove {
    position: absolute; top: -8px; right: -8px; width: 22px; height: 22px; border-radius: 50%;
    background: white; border: 1px solid #d0d0d0; color: #6b6b6b; font-size: 12px; line-height: 1;
    display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
    transition: background var(--fp-motion), color var(--fp-motion), border-color var(--fp-motion), transform var(--fp-motion);
  }
  .fp-flow-card-remove:hover { background: #d82c0d; color: white; border-color: #d82c0d; transform: scale(1.1); }
  /* Move/remove controls, top-right of a card. Laid out as a plain flex
     row of native buttons rather than an <s-stack>: see the Polaris
     shadow-DOM note near .fp-grid-table -- gap on an s-stack either does
     nothing or draws a second box around the component's own. */
  /* Always visible, not hover-revealed. The complaint that prompted this
     was that moving a step appeared impossible, so hiding the controls
     until hover would have left it just as invisible to anyone who did
     not happen to mouse over a card -- and invisible entirely on touch.
     The remove ✕ beside them has always been persistent; these match it.
     They sit at low contrast and lift on hover instead. */
  .fp-flow-card-actions {
    position: absolute; top: 6px; right: 6px;
    display: flex; gap: 3px;
    opacity: 0.55; transition: opacity var(--fp-motion);
  }
  .fp-flow-card:hover .fp-flow-card-actions,
  .fp-flow-card:focus-within .fp-flow-card-actions { opacity: 1; }
  .fp-flow-card-move {
    width: 20px; height: 20px; padding: 0; line-height: 1;
    font-size: 12px; cursor: pointer;
    background: white; color: #6b6b6b;
    border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small);
    transition: background var(--fp-motion), color var(--fp-motion), border-color var(--fp-motion);
  }
  .fp-flow-card-move:hover { border-color: var(--fp-color-brand); color: var(--fp-color-brand); }
  .fp-flow-card-move:focus-visible { outline: 2px solid var(--fp-color-focus); outline-offset: 1px; }
  .fp-flow-move-menu {
    position: absolute; top: 100%; right: 0; z-index: 20;
    margin-block-start: 4px; padding: 6px;
    display: flex; flex-direction: column; gap: 2px;
    min-width: 200px; max-width: 300px; text-align: start;
    background: white; border: 1px solid var(--fp-color-border);
    border-radius: var(--fp-radius-control); box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  }
  .fp-flow-move-menu-label {
    font-size: 11px; font-weight: 600; color: #6b6b6b;
    padding: 2px 6px 4px;
  }
  .fp-flow-move-menu button {
    text-align: start; padding: 6px 8px; font-size: 12px; cursor: pointer;
    background: none; border: none; border-radius: var(--fp-radius-small); color: inherit;
  }
  .fp-flow-move-menu button:hover { background: rgba(0,110,82,0.08); }
  .fp-flow-move-menu button:focus-visible { outline: 2px solid var(--fp-color-focus); outline-offset: -2px; }
  .fp-flow-move-cancel { color: #6b6b6b; }
  .fp-flow-add {
    margin-block-start: 8px; padding: 8px 16px; border-radius: 20px; border: 1.5px dashed var(--fp-color-border);
    background: transparent; color: var(--fp-color-subdued); font-size: 12.5px; font-weight: 600; cursor: pointer;
    transition: border-color var(--fp-motion), color var(--fp-motion), background var(--fp-motion);
  }
  .fp-flow-add:hover { border-color: var(--fp-color-brand); color: var(--fp-color-brand); background: rgba(0,110,82,0.05); }
  .fp-flow-type-menu { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-block-start: 8px; max-width: 320px; }
  .fp-flow-add-branch {
    align-self: flex-start; margin-block-start: 0; white-space: nowrap;
    padding: 8px 16px; border-radius: 20px; border: 1.5px dashed var(--fp-color-border);
    background: transparent; color: var(--fp-color-subdued); font-size: 12.5px; font-weight: 600; cursor: pointer;
    transition: border-color var(--fp-motion), color var(--fp-motion), background var(--fp-motion);
  }
  .fp-flow-add-branch:hover { border-color: var(--fp-color-brand); color: var(--fp-color-brand); background: rgba(0,110,82,0.05); }
  /* Wraps the branch label + its remove control (only rendered when a
     split has more than 2 branches, matching the floor addStepToList
     enforces) so the ✕ sits inline next to the label instead of stacked
     below the branch's whole step list. */
  .fp-flow-branch-remove {
    width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0;
    background: white; border: 1px solid #d0d0d0; color: #6b6b6b; font-size: 10px; line-height: 1;
    display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
    transition: background var(--fp-motion), color var(--fp-motion), border-color var(--fp-motion);
  }
  .fp-flow-branch-remove:hover { background: #d82c0d; color: white; border-color: #d82c0d; }

  /* Backdrop fade + panel slide, instead of the display:none/block snap
     JS toggles (see openStepConfigPanel/closeStepConfigPanel) -- a
     side-drawer that just appears reads as unfinished; opacity/transform
     transitions are cheap and don't require any JS change, since the JS
     already just adds/removes an .fp-open class alongside the existing
     display toggle (kept for layout removal when fully closed -- a
     transitioned but still-"display:block" panel would stay in the
     accessibility tree and tab order while "closed"). */
  .fp-step-backdrop {
    position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 1001;
    opacity: 0; transition: opacity var(--fp-motion);
  }
  .fp-step-backdrop.fp-open { opacity: 1; }
  .fp-step-panel {
    position: fixed; top: 0; right: 0; height: 100%; width: 360px; z-index: 1002;
    background: white; box-shadow: -16px 0 48px rgba(0,0,0,0.14); padding: 24px; overflow-y: auto;
    transform: translateX(100%); transition: transform var(--fp-motion);
  }
  .fp-step-panel.fp-open { transform: translateX(0); }
  .fp-step-panel h3 { margin: 0 0 18px; }

  .fp-usage-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; font-size: 14px; }
  .fp-usage-bar { height: 8px; border-radius: var(--fp-radius-small); background: #ece9e4; overflow: hidden; }
  .fp-usage-bar-fill { height: 100%; background: #006e52; border-radius: var(--fp-radius-small); }

  /* Billing plan cards -- plain CSS grid/divs, not <s-grid>/<s-box> (see
     the JS comment in loadBilling for why). Originally a fixed-width
     (220-260px), centered layout deliberately -- but reported live as
     looking wrong on both mobile (narrow cards with dead margin on both
     sides) and desktop (visible gap on the left/right edges relative to
     the usage bars and every other section's content, which all run the
     section's full width). Changed to the same "auto-fit, no upper cap"
     pattern .fp-quickaction-grid already uses successfully: cards stretch
     to fill the available width, so their edges always land on the same
     inset as the usage bars above them, on every screen size, and the
     gap between cards (16px) reads as consistent with the rest of the
     section's spacing rather than being its own one-off value. CSS
     Grid's real default (align-items: stretch) gives every card in a row
     equal height for free here; margin-block-start: auto on the one
     action button anchors it to the bottom of that stretched height
     regardless of how much text sits above it (a "Current plan" badge on
     Free, for instance), so all three line up evenly. */
  .fp-plan-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
  .fp-plan-card {
    display: flex; flex-direction: column; gap: 8px; box-sizing: border-box;
    padding: 16px; background: white; border: 1px solid #e3e3e3; border-radius: var(--fp-radius-card);
  }
  /* The auto margin has to sit on a PLAIN div, not on the s-button itself.
     An <s-*> host does not behave as a normal flex item (it draws its box
     inside shadow DOM -- see this file's own notes and CLAUDE.md), so
     a rule targeting s-button directly silently did
     nothing. That went unnoticed while every card's label wrapped to the
     same number of lines and the buttons therefore lined up by accident;
     shortening the labels to "N orders/month" made some cards wrap and
     others not, which exposed it. */
  .fp-plan-card-action { margin-block-start: auto; }

  .fp-editor-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
  /* Review fix: the preview panel's top lines up with the top of the
     Template Name INPUT, not its label (26px = Polaris label + gap). */
  #tpl-legacy-view .fp-preview { margin-top: 26px; }
  /* Review fix: the preview document is a PAGE, not a mirror of the code
     column's height -- aspect ratio follows the Document size select at
     the panel's bottom (A4 default). */
  #tpl-legacy-view .fp-preview-frame { flex: none; aspect-ratio: 210 / 297; min-height: 0; height: auto; }
  .fp-preview-size { padding: 0 20px 16px; }
  /* Cool neutral gray + real border, not the warm tan this used to be --
     see .fp-block-palette/.fp-block-props's own comment for the same fix
     applied to the block editor's panels; this is the legacy-view
     equivalent. */
  .fp-preview { display: flex; flex-direction: column; background: #f6f6f7; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); }
  .fp-preview-label { padding: 14px 18px 10px; font-size: 12px; font-weight: 700; color: #6b6b6b; }
  .fp-preview-frame { flex: 1; min-height: 260px; border: 1px solid #b8bbc0; background: white; margin: 0 20px 20px; border-radius: var(--fp-radius-small); }

  /* Canva-style block editor (2026-08-13, restyled same day after a real
     screenshot review caught it reading as an unstyled dev tool -- see
     renderTemplateEditorPage's own comment). Three regions per the docs'
     two-column visual-editor guidance, extended to a third (properties)
     panel -- the same primary-canvas/secondary-controls shape, just with
     the secondary side split into "what can I add" (left) and "how is
     the selected thing configured" (right), which is the standard triptych
     for this class of editor (Word's own picture-formatting pane works
     the same way). All three now sit inside one real <s-section> card
     (HTML side) rather than floating as loose divs on the bare page
     background. */
  /* Two deliberate rows: fields, then toggles left / actions right.
     The old single flex-wrap line broke wherever the width happened to
     run out (three checkboxes, two buttons, then three more on the next
     line -- reported 2026-09-05 as goofy, correctly). */
  .fp-block-toolbar { display: flex; flex-direction: column; gap: 10px; }
  .fp-toolbar-line { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; }
  .fp-toolbar-line-2 { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; }
  .fp-toolbar-checks { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
  .fp-toolbar-btns { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
  .fp-block-layout { display: grid; grid-template-columns: 200px 1fr 260px; gap: 16px; align-items: start; }
  /* Cool neutral gray, not the warm tan this used to be -- Shopify's own
     admin palette (and every other gray in this file) is cool-neutral;
     a beige tint next to real Polaris surfaces read as visibly off-brand.
     A visible border matters more now than it did before: these panels
     sit inside a white <s-section> card, so without one they'd have
     nothing distinguishing them from that card's own white background. */
  .fp-block-palette, .fp-block-props {
    background: #f6f6f7; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control);
    padding: 14px; display: flex; flex-direction: column; gap: 10px;
  }
  .fp-panel-heading { font-size: 12px; font-weight: 700; color: #6b6b6b; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.03em; }
  /* Sub-group label inside the properties panel (the crop controls).
     Used since the crop feature shipped but never styled -- found by the
     orphaned-class guard, not by eye. */
  .fp-panel-subheading {
    font-size: 10.5px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
    color: #5c5f62; margin: 10px 0 4px;
  }
  /* "Add to a Shopify notification" modal's rendered-button preview --
     a plain neutral card so the button reads the way it would sitting
     inside an actual email body, not the app's own admin chrome. */
  .fp-snippet-preview { padding: 16px; background: #f6f6f7; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-control); text-align: center; }
  /* Email embed button editor (Templates > Email link). Native inputs on
     purpose: instant input events drive the live preview, and the modal
     is excluded from save-bar dirty tracking anyway. */
  .fp-btneditor { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; }
  .fp-btneditor-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--fp-color-subdued); }
  .fp-btneditor-wide { grid-column: 1 / -1; }
  .fp-btneditor-field input[type="text"], .fp-btneditor-field input[type="number"] { padding: 6px 8px; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small); font-size: 13px; color: var(--fp-color-text); }
  .fp-btneditor-field input[type="color"] { inline-size: 100%; block-size: 32px; padding: 2px; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small); background: #ffffff; }
  .fp-btneditor-field input[type="checkbox"] { inline-size: 18px; block-size: 18px; margin-block-start: 4px; }
  @media (max-width: 480px) { .fp-btneditor { grid-template-columns: 1fr; } }

  /* Layers panel -- topmost block (highest z) listed first, matching
     every real design tool's own top-to-bottom = front-to-back
     convention. Capped height + its own scroll so a template with many
     blocks doesn't push the palette tiles below the fold. */
  .fp-layer-list { display: flex; flex-direction: column; gap: 4px; max-height: 220px; overflow-y: auto; }
  /* #8a8a8a measured 3.45:1 on white, under the 4.5 AA needs for body
     text. Built for Shopify lists failing WCAG AA contrast as a
     rejection reason, so this is a requirement, not a preference. */
  .fp-layer-empty { font-size: 12px; color: #6b6b6b; padding: 4px 2px; }
  .fp-layer-row {
    display: flex; align-items: center; gap: 6px; padding: 6px 8px;
    background: white; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-card);
    cursor: pointer; font-size: 12px; color: #444;
  }
  .fp-layer-row:hover { border-color: rgba(0,110,82,0.5); }
  .fp-layer-row.fp-layer-selected { border-color: var(--fp-color-brand); background: rgba(0,110,82,0.06); }
  .fp-layer-icon { display: flex; flex-shrink: 0; color: #6b6b6b; }
  .fp-layer-icon svg { width: 14px; height: 14px; }
  .fp-layer-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .fp-layer-actions { display: flex; gap: 2px; flex-shrink: 0; }
  .fp-layer-btn {
    display: flex; align-items: center; justify-content: center; width: 20px; height: 20px;
    background: transparent; border: none; border-radius: var(--fp-radius-small); color: #6b6b6b; cursor: pointer; padding: 0;
  }
  .fp-layer-btn:hover:not(:disabled) { background: #ececec; color: #202223; }
  .fp-layer-btn:disabled { opacity: 0.3; cursor: default; }
  /* Measured against its OWN hover panel rather than the card: #d82c0d
     on #fbeae9 is 4.21:1, just under AA. The panel is the whole point
     of the hover state, so the text darkens instead. */
  .fp-layer-btn-delete:hover { background: #fbeae9; color: #b5280c; }
  .fp-slider-field label { display: block; font-size: 0.8125rem; color: #303030; margin-bottom: 4px; }
  .fp-radius-field { margin-block: 4px; }
  .fp-radius-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
  .fp-radius-head label { font-size: 0.8125rem; color: #303030; }
  .fp-radius-corners { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .fp-slider-row { display: flex; align-items: center; gap: 10px; }
  .fp-slider-row input[type="range"] { flex: 1; }
  .fp-slider-row span { min-width: 2.5em; text-align: right; font-variant-numeric: tabular-nums; color: #6b6b6b; font-size: 0.8125rem; }
  /* A drag handle is a meaningful UI component, not decoration, so it
     falls under non-text contrast (1.4.11, 3:1). #c4c4c4 measured
     1.74:1 -- effectively invisible against the card. */
  .fp-layer-grip { display: flex; flex-shrink: 0; color: #8a8a8a; cursor: grab; touch-action: none; }
  .fp-layer-grip:active { cursor: grabbing; }
  .fp-layer-row:hover .fp-layer-grip { color: #6b6b6b; }
  .fp-layer-dragging { opacity: 0.5; }
  .fp-layer-drag-over-top { box-shadow: inset 0 2px 0 0 var(--fp-color-brand); }
  .fp-layer-drag-over-bottom { box-shadow: inset 0 -2px 0 0 var(--fp-color-brand); }

  /* Block-type picker, 2026-08-13 redesign -- a Canva-style icon-tile grid
     (icon centered above a short label) replacing three plain stacked
     text buttons that, worse, all shared one identical icon (real bug,
     confirmed live). Each tile is a plain <button>, not <s-button> --
     an icon-above-label tile isn't a shape Polaris's button component
     supports, same reasoning as every other hand-rolled interactive
     element in this file. */
  .fp-block-tile-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .fp-block-tile {
    display: flex; flex-direction: column; align-items: center; gap: 6px;
    padding: 14px 6px; background: white; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-card);
    cursor: pointer; color: #444; font: inherit; font-size: 12px; font-weight: 600;
    transition: border-color var(--fp-motion), box-shadow var(--fp-motion), transform var(--fp-motion), color var(--fp-motion);
  }
  .fp-block-tile:hover {
    border-color: var(--fp-color-brand); color: var(--fp-color-brand);
    box-shadow: 0 4px 12px rgba(0,0,0,0.08); transform: translateY(-1px);
  }
  .fp-block-tile:focus-visible { outline: 2px solid var(--fp-focus-ring); outline-offset: 2px; }
  /* No :last-child full-width rule needed here -- 10 tiles is an even
     count in this 2-column grid (5 clean rows), unlike the odd counts
     (7, then 9) this grid has had at earlier points in this file's
     history, each of which needed that rule. Re-add it if a future
     addition makes the count odd again. */

  .fp-block-canvas-wrap {
    overflow: auto; background: #d9d9d9; border: 1px solid var(--fp-color-border);
    border-radius: var(--fp-radius-control); padding: 18px;
    /* flex: 1 is load-bearing. This is a flex ITEM of .fp-block-canvas-outer
       (which exists only so the zoom bubble can be positioned against the
       canvas viewport), and without it the work area shrinks to the width
       of the page inside it -- confirmed live 2026-08-20 at 1440px: the
       grey area was 334px wide inside a 731px column, leaving ~400px of
       bare card between the canvas and the Properties panel, and "Fit"
       zoom then fitted the page to that same too-small box. The
       text-align: center below can't fix this: centering content inside a
       box that has already shrink-wrapped to that content is a no-op. */
    flex: 1; min-width: 0;
    /* Centers the inline-block zoom wrapper (and so the page) in the grey
       work area -- audited at 1440px with the page sitting flush left and
       all the dead space on the right, which read as unfinished next to
       how Canva/Figma center their page. text-align is inherited, so the
       zoom wrapper resets it (see .fp-block-canvas-zoom) before it can
       reach any text block's content. */
    text-align: center;
    /* Sized to the actual remaining viewport (100vh minus the admin top
       bar, page heading row, and the toolbar card above this one) rather
       than a flat 70vh -- the goal is the whole editor fitting in one
       screen with no page-level scroll, only the canvas's own internal
       scroll for content that's genuinely bigger than the viewport.
       min-height is a floor for short viewports, not a target. */
    height: calc(100vh - 280px); min-height: 320px;
    /* Thin, low-contrast scrollbars instead of the browser's chunky
       default -- purely cosmetic, this canvas area is the single most
       "look at this app" surface on the page. Firefox + Chromium syntax
       both included; a browser supporting neither just keeps its own
       default, harmless either way. */
    scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.25) transparent;
  }
  .fp-block-canvas-wrap::-webkit-scrollbar { width: 10px; height: 10px; }
  .fp-block-canvas-wrap::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: var(--fp-radius-small); }
  .fp-block-canvas-wrap::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.32); }
  .fp-block-canvas-wrap::-webkit-scrollbar-track { background: transparent; }
  /* zoom (not transform: scale) is what actually shrinks the wrapper's
     own layout box along with its visual size, so the scrollable area
     stays correctly sized at any zoom level instead of leaving dead
     space equal to the untransformed original dimensions -- exactly
     the outcome a merchant would read as "the canvas is broken" if
     transform were used instead. Non-standard but broadly supported in
     every Chromium build, which is what Shopify admin actually runs in;
     a browser that doesn't support it just shows the canvas at 100%,
     a harmless fallback, not a broken one. */
  .fp-block-canvas-zoom { display: inline-block; text-align: left; }
  /* Wraps the scroll area so the zoom bubble can be positioned against the
     canvas viewport rather than scrolling away with its contents. */
  .fp-block-canvas-outer { position: relative; flex: 1; min-width: 0; display: flex; }
  .fp-canvas-zoombar {
    position: absolute; left: 12px; bottom: 12px; z-index: 20;
    display: flex; align-items: stretch; gap: 1px; overflow: hidden;
    background: rgba(255,255,255,0.92); backdrop-filter: blur(6px);
    border: 1px solid var(--fp-color-border); border-radius: 999px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.14);
  }
  .fp-canvas-zoombar button {
    min-width: 34px; height: 30px; padding: 0 9px; border: 0; background: transparent;
    font: inherit; font-size: 14px; line-height: 1; color: var(--fp-color-text);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
  }
  .fp-canvas-zoombar button:hover { background: rgba(0,0,0,0.06); }
  .fp-canvas-zoombar button:active { background: rgba(0,0,0,0.11); }
  #tpl-zoom-reset { min-width: 52px; font-size: 12px; font-variant-numeric: tabular-nums; }
  .fp-block-canvas {
    position: relative; background: white; margin: 0 auto; box-shadow: 0 4px 20px rgba(0,0,0,0.22), 0 1px 3px rgba(0,0,0,0.15);
  }
  /* Grid as an overlay ABOVE the blocks (pointer-events: none), not the
     canvas background -- behind filled blocks it was invisible on any
     real template ("why is snap to grid behind everything"). */
  .fp-canvas-gridlayer {
    /* Above every block (block z-index is small integers) but BELOW the
       app's own overlays -- 100000 punched through the Preview overlay
       (z-index 1003) and drew gridlines over the rendered document,
       found live within the hour. */
    position: absolute; inset: 0; pointer-events: none; z-index: 999;
    background-image:
      linear-gradient(to right, rgba(0,110,82,0.22) 1px, transparent 1px),
      linear-gradient(to bottom, rgba(0,110,82,0.22) 1px, transparent 1px);
  }

  /* Properties empty state -- an icon + centered message, replacing a
     single line of top-left text with a large empty void below it
     (confirmed live: read as unfinished, not intentional). */
  .fp-block-props-empty {
    display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center;
    color: var(--fp-color-subdued); font-size: 12.5px; padding: 24px 8px; opacity: 0.85;
  }
  /* fp-tpl-block, not fp-block -- .fp-block is already a widely-used class
     (every <s-section class="fp-block"> on every page, for inter-section
     spacing, see its own rule earlier in this file). Reusing the bare
     name here for the Canva editor's draggable block element would have
     been a real collision (harmless today only by accident -- margin-
     block-end doesn't affect an absolutely positioned element's top/left
     -- but a future rule scoped to ".fp-block" would silently leak into
     the template canvas). Caught in the 2026-08-13 polish pass; renamed
     before it could bite. */
  .fp-tpl-block {
    position: absolute; cursor: move; outline: 1px dashed transparent; overflow: hidden;
    transition: outline-color var(--fp-motion), box-shadow var(--fp-motion);
    /* A block is something you drag with OUR code, so the browser's own
       two competing press-and-move gestures are both turned off here:
       text selection (which paints a selection over a text block mid-drag)
       and native element dragging. The image case additionally carries
       draggable="false" on the <img> itself, since -webkit-user-drag has
       never been standardised and Firefox ignores it. */
    user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
  }
  .fp-tpl-block img { pointer-events: none; -webkit-user-drag: none; }
  .fp-tpl-block:hover { outline-color: rgba(0,110,82,0.5); }
  .fp-tpl-block.fp-block-selected { outline: 2px solid var(--fp-color-brand); box-shadow: 0 2px 10px rgba(0,110,82,0.18); }
  /* Eight handles: four corners and four edges. Previously ONE
     bottom-right corner handle, 12px across. That is both halves of the
     complaint: a 12px target is a fiddly thing to hit, and resizing from
     only one corner means the opposite edge can never be the one that
     stays put, so you drag the block back afterwards every time. The
     visible dot stays small so it does not bury a small block; ::before
     supplies a much bigger invisible hit area around it. */
  .fp-block-rotate-handle { display: none; position: absolute; left: 50%; top: -26px; margin-left: -6px; width: 12px; height: 12px; border-radius: 50%; background: #ffffff; border: 2px solid var(--fp-color-brand); cursor: grab; }
  .fp-block-rotate-handle::before { content: ""; position: absolute; left: 50%; top: 12px; width: 2px; height: 12px; margin-left: -1px; background: var(--fp-color-brand); }
  .fp-block-rotate-handle::after { content: ""; position: absolute; inset: -8px; }
  .fp-block-selected .fp-block-rotate-handle { display: block; }
  .fp-num-field { display: flex; flex-direction: column; gap: 4px; font-size: 0.8125rem; color: #303030; min-width: 0; flex: 1; }
  .fp-num-field input { padding: 6px 8px; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small); font-size: 13px; color: var(--fp-color-text); width: 100%; box-sizing: border-box; }
  .fp-num-field input::-webkit-inner-spin-button { opacity: 1; }
  .fp-cond-field { display: flex; flex-direction: column; gap: 8px; margin-block: 4px; }
  .fp-table-col-head { display: flex; align-items: center; gap: 4px; }
  .fp-tcol-move { border: 1px solid var(--fp-color-border); background: #ffffff; border-radius: var(--fp-radius-small); width: 22px; height: 22px; font-size: 9px; cursor: pointer; color: #303030; }
  .fp-tcol-move:disabled { opacity: 0.35; cursor: default; }
  .fp-tbl-acc { border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small); margin-block: 4px; background: #fafafa; }
  .fp-tbl-acc > summary { cursor: pointer; padding: 8px 10px; font-size: 12.5px; font-weight: 600; color: var(--fp-color-text); user-select: none; }
  .fp-tbl-acc[open] > summary { border-block-end: 1px solid var(--fp-color-border); }
  .fp-tbl-acc-body { padding: 8px 10px; display: flex; flex-direction: column; gap: 8px; background: #ffffff; border-radius: 0 0 var(--fp-radius-small) var(--fp-radius-small); }
  .fp-tsum-label { flex: 1; min-width: 0; padding: 5px 8px; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small); font-size: 12.5px; color: var(--fp-color-text); }
  .fp-block-resize-handle {
    display: none; position: absolute; width: 10px; height: 10px; border-radius: 50%;
    background: white; border: 2px solid var(--fp-color-brand);
    box-shadow: 0 1px 3px rgba(0,0,0,0.28); z-index: 3;
    transition: transform var(--fp-motion), background-color var(--fp-motion);
  }
  .fp-block-resize-handle::before { content: ""; position: absolute; inset: -8px; }
  .fp-block-resize-handle:hover { transform: scale(1.35); background: var(--fp-color-brand); }
  .fp-block-selected .fp-block-resize-handle { display: block; }
  .fp-block-resize-handle[data-handle="nw"] { left: -6px;  top: -6px;    cursor: nwse-resize; }
  .fp-block-resize-handle[data-handle="ne"] { right: -6px; top: -6px;    cursor: nesw-resize; }
  .fp-block-resize-handle[data-handle="se"] { right: -6px; bottom: -6px; cursor: nwse-resize; }
  .fp-block-resize-handle[data-handle="sw"] { left: -6px;  bottom: -6px; cursor: nesw-resize; }
  .fp-block-resize-handle[data-handle="n"]  { left: 50%; top: -6px;    margin-left: -5px; cursor: ns-resize; }
  .fp-block-resize-handle[data-handle="s"]  { left: 50%; bottom: -6px; margin-left: -5px; cursor: ns-resize; }
  .fp-block-resize-handle[data-handle="w"]  { top: 50%;  left: -6px;   margin-top: -5px;  cursor: ew-resize; }
  .fp-block-resize-handle[data-handle="e"]  { top: 50%;  right: -6px;  margin-top: -5px;  cursor: ew-resize; }
  /* Too small to hold eight handles without them overlapping each other:
     show corners only, which stay unambiguous at any size. */
  .fp-tpl-block[data-small="1"] .fp-block-resize-handle[data-handle="n"],
  .fp-tpl-block[data-small="1"] .fp-block-resize-handle[data-handle="s"],
  .fp-tpl-block[data-small="1"] .fp-block-resize-handle[data-handle="w"],
  .fp-tpl-block[data-small="1"] .fp-block-resize-handle[data-handle="e"] { display: none; }
  /* Smart alignment guides -- created/destroyed on demand by
     ensureAlignGuidesEl, only ever present mid-drag. Magenta is
     Figma/Sketch/most design tools' own convention for this, distinct
     from the brand-green selection outline and the gray grid dots. */
  .fp-align-guides { position: absolute; inset: 0; pointer-events: none; z-index: 50; }
  .fp-align-guide-v { position: absolute; top: 0; bottom: 0; width: 1px; background: #ff3399; }
  .fp-align-guide-h { position: absolute; left: 0; right: 0; height: 1px; background: #ff3399; }
  .fp-block-props-empty { font-size: 12.5px; color: var(--fp-color-subdued); }
  .fp-block-props s-text-field, .fp-block-props s-select { width: 100%; }
  .fp-block-props-row { display: flex; gap: 8px; }
  .fp-block-props-row > * { flex: 1; min-width: 0; }
  /* Line-items column editor -- see renderBlockProps's own note for why
     each column is a stacked group rather than a three-up row. */
  .fp-table-col { display: grid; gap: 6px; padding: 8px 0; border-block-end: 1px solid var(--fp-color-border); }
  .fp-table-col:last-of-type { border-block-end: none; }
  .fp-block-props .fp-table-col-name { display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 12px; color: var(--fp-color-text); }
  .fp-table-col-fields { display: grid; gap: 6px; }
  .fp-block-image-thumb { width: 100%; border-radius: var(--fp-radius-small); border: 1px solid var(--fp-color-border); display: block; }
  .fp-block-props label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--fp-color-subdued); }
  .fp-crop-block { display: grid; gap: 6px; margin-top: 4px; }
  /* Scoped through .fp-block-props deliberately. The panel sets
     ".fp-block-props label { display: flex; flex-direction: column }" for
     its ordinary stacked fields, and a crop row IS a <label>, so that
     rule (specificity 0,1,1) beat a bare .fp-crop-row (0,1,0) and stacked
     every crop control vertically -- label, then a full-width slider,
     then the value underneath, three rows deep per side. Confirmed live.
     Matching the panel's own scope wins it back at 0,2,0. */
  .fp-block-props .fp-crop-row { display: grid; grid-template-columns: 52px 1fr 38px; align-items: center; gap: 8px; margin: 0; }
  .fp-crop-row input[type="range"] { width: 100%; min-width: 0; }
  .fp-crop-val { font-size: 11px; color: var(--fp-color-subdued); font-variant-numeric: tabular-nums; text-align: right; }
  .fp-align-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-block-end: 8px; }
  .fp-align-btn {
    display: flex; align-items: center; justify-content: center; padding: 8px; background: white;
    border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-small); color: #444; cursor: pointer;
    transition: border-color var(--fp-motion), color var(--fp-motion), background var(--fp-motion);
  }
  .fp-align-btn:hover { border-color: var(--fp-color-brand); color: var(--fp-color-brand); background: #f6f6f7; }
  .fp-align-btn:focus-visible { outline: 2px solid var(--fp-focus-ring); outline-offset: 2px; }

  .fp-preview-overlay {
    position: fixed; inset: 0; z-index: 1003; background: white; display: flex; flex-direction: column;
    opacity: 0; transition: opacity var(--fp-motion);
  }
  .fp-preview-overlay.fp-open { opacity: 1; }
  .fp-preview-overlay-head {
    display: flex; align-items: center; justify-content: space-between; padding: 14px 20px;
    font-size: 13px; font-weight: 700; color: #6b6b6b; border-bottom: 1px solid var(--fp-color-border);
  }
  .fp-preview-overlay .fp-preview-frame { flex: 1; margin: 0; border-radius: 0; }

  /* Variable picker, 2026-08-13 redesign -- a single "Insert variable"
     button opening a clean popover menu of plain-English names (real
     feedback: "friendly names that dont look like code, people use the
     visual editor to escape any level of code"), replacing a permanently
     -visible strip of chips that showed raw {{ Liquid }} syntax. */
  .fp-var-trigger-wrap { position: relative; display: inline-block; }
  .fp-var-menu {
    position: absolute; top: calc(100% + 4px); left: 0; z-index: 20; width: 220px; max-height: 280px;
    overflow-y: auto; background: white; border: 1px solid var(--fp-color-border); border-radius: var(--fp-radius-card);
    box-shadow: 0 4px 16px rgba(0,0,0,0.14); padding: 6px;
  }
  .fp-var-menu-group {
    font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
    color: var(--fp-color-subdued); padding: 8px 8px 4px;
  }
  .fp-var-menu-item {
    display: block; width: 100%; text-align: left; background: none; border: none; border-radius: var(--fp-radius-small);
    padding: 7px 8px; font: inherit; font-size: 13px; color: #202223; cursor: pointer;
    transition: background var(--fp-motion);
  }
  .fp-var-menu-item:hover, .fp-var-menu-item:focus-visible { background: #f1f1f1; outline: none; }
  .fp-var-menu-name { display: block; }
  .fp-var-menu-token { display: block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #6b6b6b; margin-block-start: 1px; }

  .fp-starters { display: flex; gap: 10px; margin-bottom: 16px; }
  .fp-starter-card {
    flex: 1; border: 1px solid #d0d0d0; border-radius: var(--fp-radius-control); padding: 13px; cursor: pointer;
    text-align: left; background: white;
    transition: border-color var(--fp-motion), box-shadow var(--fp-motion), transform var(--fp-motion);
  }
  .fp-starter-card:hover { border-color: #006e52; box-shadow: 0 4px 12px rgba(0,0,0,0.08); transform: translateY(-1px); }
  .fp-starter-card strong { display: block; font-size: 13px; margin-bottom: 2px; }
  .fp-starter-card span { font-size: 11.5px; color: #6b6b6b; }

  /* Mobile pass, 2026-08-05 -- reported live on a real phone (screenshots,
     not a resized desktop browser): tables cut off, the template editor's
     two-column layout unreadable, the workflow toolbar's Enabled checkbox
     crammed in with the step-add buttons. Every fix here is scoped to
     max-width:640px specifically so desktop layout (already screenshot-
     verified correct) is untouched. */
  /* Template editor at tablet/split-screen widths. The three-column
     workspace (200px palette + canvas + 260px properties) only had a
     desktop layout and a fully-stacked phone layout at 640px, with
     nothing in between -- confirmed live 2026-08-20 at a ~690px admin
     viewport: the two fixed side columns ate everything and the canvas
     was left a ~95px sliver, narrower than the page thumbnail inside it.
     Properties drops to its own full-width row under the canvas here,
     which is the panel that tolerates being below the fold; the palette
     stays beside the canvas because adding a block is the thing you do
     WHILE looking at the page. */
  @media (max-width: 1080px) {
    .fp-block-layout { grid-template-columns: 180px 1fr; }
    .fp-block-props { grid-column: 1 / -1; }
    .fp-block-canvas-wrap { height: calc(100vh - 340px); min-height: 300px; }
  }

  @media (max-width: 640px) {
    /* Tighter table padding gives the narrow "auto" actions column more
       room before its content is forced to wrap -- see .fp-row-actions
       above for the other half of the Edit/Delete-on-one-line fix. */
    .fp-grid-header, .fp-grid-cell { padding: 8px 10px; font-size: 13px; }

    /* Job log has 6 columns -- no amount of padding/font-size reduction
       makes Step+Workflow+Event+Status+Created+actions fit a ~360px
       screen without truncating content, so this is the one table that
       gets a real horizontal scroll instead: fixed px tracks (wider than
       any phone viewport) force the grid to overflow its own box, and
       overflow-x:auto (overriding the base .fp-grid-table's overflow:
       hidden, which was the actual cause of the reported cutoff -- it
       clips instead of scrolling) lets the merchant swipe to see the
       rest, same pattern as any responsive data table. */
    .fp-stock-notify-table { grid-template-columns: 160px 100px 90px 90px 70px; overflow-x: auto; }

    /* Workers stays 3 columns, so it fits without scrolling -- just give
       the actions column relatively more room than desktop's 2fr 1fr auto
       now that padding is already tighter above. */

    /* The detailed 6-column Workers-page table doesn't fit a phone width
       at all -- same fixed-track + real horizontal scroll pattern as Job
       log/Quotes/Templates above, not a squeeze into fewer columns. */

    /* Templates USED to fit as 3 flexible columns, but its actions cell
       has since grown to four buttons (Customer downloads toggle / Email
       link / Edit / Delete) -- audited at 390px: the whole cell's
       min-content width pushed the grid past the card edge and the base
       .fp-grid-table's overflow:hidden silently clipped Email link/Edit/
       Delete out of existence, leaving no way to edit or delete a
       template from a phone. Same fix as the Job log/Quotes/Email tables
       below: fixed tracks + real horizontal scroll. */

    /* Email automations has 4 columns (Name/Trigger/Status/actions), not
       3 like Workers/Templates -- reusing their 3-track override here was
       a real bug (not a viewport-detection question, just wrong CSS): a
       4-cell-per-row grid given only 3 explicit tracks wraps its 4th cell
       onto a new implicit row on every single row, which is exactly what
       "Order paid" and "enabled"/"Edit" landing on their own misaligned
       lines under the wrong headers was -- reported live as text reading
       like "Customer Invoicd" (actually "Customer Invoice" and "enabled"
       running together across the misaligned grid, not a data bug).
       Fixed the same way as the 6-column Job log table: real horizontal
       scroll instead of trying to cram 4 columns into ~360px. */

    /* Template editor: side-by-side form+preview only works with real
       width to give each column. Reported live as the preview pane
       rendering on top of the form fields with just a sliver of each
       visible -- that's the two 1fr tracks each refusing to shrink below
       their content's min-content size (a textarea, an iframe) on a
       ~360px modal, overflowing the columns past each other rather than
       wrapping. Stacking to one column (form, then preview, in document
       order) is the standard fix -- no width fight, just a taller modal. */
    .fp-editor-layout { grid-template-columns: 1fr; }
    .fp-editor-layout .fp-preview { min-height: 320px; }

    .fp-block-layout { grid-template-columns: 1fr; }
    .fp-block-canvas-wrap { height: 60vh; }

  }
