  /* Same visual system as agent.html (docs/CODE-AGENT-PLAN.md Phase 6:
     "reuse agent.html" — done as a NEW file with the same look, not an
     edit to that file, so the site-builder's own tested claim/publish/
     history flows stay completely untouched by this product line. */
  *,*::before,*::after{box-sizing:border-box}
  *[hidden], [hidden]{display:none !important;pointer-events:none !important;visibility:hidden !important}
  :root{

    /* Interactive hover, per theme.

       --line-hover is the border that lights up on a button, chip, pill or
       tile. It was the literal pale blue #c6dbe7 in fifteen rules: a colour
       tuned for a white page, which on a dark surface reads as a bright
       outline drawn AROUND the control rather than a highlight OF it.

       --wash-rgb is the ink of a hover FILL and of the scrollbar thumb, as a
       bare RGB triple so every surface keeps the alpha it was tuned with -
       there are ten distinct ones, and collapsing them into two tokens would
       restyle a lot of surfaces to fix a bug about their colour. Hardcoded
       near-black, a hover darkened on light (right) and darkened on dark
       (invisible). Only the colour flips; no alpha changes.

       Deliberately NOT applied to the shadows, or to the chrome on .pline /
       .art-stack / the [data-theme="light"] card rules: those sit on an
       explicitly white background or are shadows, and ink is the correct
       colour for both in either theme. */
    --line-hover:#bdb7b1;
    --wash-rgb:18,32,46;
    --paper:#f6f5f4; --paper-2:#ebeced; --card:#fcfcfc;
    --ink:#1b252b; --ink-2:#5c6062; --mut:#8a8f91;
    --accent:#1aa6df; --accent-2:#0f8ec4; --accent-ink:#04202e;
    --tint:#c7e8fa; --tint-2:#e7f5fd;
    --stone:#dcd8d4; --dark:#0d2338; --line:#cfc9c3; --navy-800:#0b1e33;
    --disp:'Jost','Century Gothic',system-ui,sans-serif;
    --body:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
    --ease:cubic-bezier(.16,1,.3,1);
    --th-dot:#2a241f;
  }
  :root[data-theme="dark"]{
    --line-hover:rgba(255,255,255,.22);
    --wash-rgb:255,255,255;
    --paper:#18191b; --paper-2:#212225; --card:#26282b;
    --ink:#f5f9fc; --ink-2:#9ea3a6; --mut:#7a7f82;
    --tint:#3d3428; --tint-2:#2d2420;
    --stone:#2a2c2f; --dark:#0a0908; --line:#33363a; --navy-800:#0d0b0a;
    --accent:#1aa6df; --accent-2:#118cc0; --accent-ink:#e6edf3;
    --th-dot:#f6f1e8;
  }


  html, body {
    background: var(--paper);
    overscroll-behavior-x: none;
    overflow-x: hidden !important;
    max-width: 100vw !important;
    width: 100% !important;
  }
  /* --ease-sb: a long decelerating curve for the sidebar. The old .22s
     var(--ease) is what read as snappy to the point of glitchy. */
  /* Measured off the reference screenshot, then divided by the display
     scaling it was captured at (see --sb-w). The proportions below — 8px
     gutter, 40px rows on a 45px pitch, 22px icon slot, 12px icon-to-label
     gap — are scale-independent and stay put; only --sb-w moves. */
  :root{ --rail:44px; --sb-w:238px; --sb-pad:6px; --ease-sb:cubic-bezier(.32,.72,0,1);
         /* Status colours for the rail's live dots, matching /projects and
            /deployments — the copied sidebar CSS references all three. */
         --ok:#22a565; --warn:#e0a11a; --danger:#d92d20; }
:root{
  /* One scrim for every overlay — the mobile sidebar drawer and the desktop
     settings modal alike. There used to be three different ones: 88% paper on
     the builder (which washed the page out almost white), rgba(0,0,0,.65) on
     /projects and /deployments, and rgba(8,7,6,.32)+blur(14px) for settings.
     Same gesture, three different answers.

     Built from --paper rather than black so it stays warm and follows the
     theme by itself. 52% and a light blur: enough to push the page back and
     make the panel clearly on top, while leaving the content readable
     underneath instead of erasing it. */
  --scrim: color-mix(in srgb, var(--paper) 52%, transparent);
  --scrim-blur: blur(5px) saturate(105%);
}
  .ag-main, .ag-split, .ag {
    overflow-x: hidden !important;
    max-width: 100vw !important;
    box-sizing: border-box !important;
  }
  /* .ag-hero is deliberately NOT in the guard above. It used to be, and
     that was the real cause of the composer glow's hard left/right cut:
     on the new-chat screen .ag.is-new turns .ag-hero into a real flex box
     (see below), and its width is .ag's CONTENT box — the padding is
     already spent — so an overflow-x:hidden there put the clip line
     exactly on the composer's own edge. Horizontal bleed budget was 0px
     on the one screen where the glow is the whole point, which is why
     widening .ag's padding appeared to do nothing.
     It cannot overflow on its own: it's a flex column whose children are
     all width:100%;max-width:720px;align-self:center. .ag/.ag-split/
     .ag-main/html/body still clip, so the horizontal-scroll bug the guard
     was added for stays fixed. */
  .ag-hero { max-width: 100%; min-width: 0; box-sizing: border-box; }
  body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--body);-webkit-font-smoothing:antialiased;
    padding:0 env(safe-area-inset-right) 0 env(safe-area-inset-left)}
  *{-webkit-tap-highlight-color:transparent}
  a{color:inherit;text-decoration:none}
  h1,h2,h3,h4{font-family:var(--disp);font-weight:600;letter-spacing:-.03em;line-height:1.04;margin:0}
  button{font:inherit}

  .nav{position:static;z-index:40;display:flex;align-items:center;gap:20px;padding:14px clamp(18px,4vw,40px);background:var(--paper)}
  .brand{display:flex;align-items:center;gap:10px;font-family:var(--disp);font-weight:800;font-size:1.1rem;color:var(--ink)}
  .nav .lnk{margin-left:auto;display:flex;gap:2px;align-items:center}
  .nav-tag{display:inline-flex;align-items:center;background:var(--accent);color:#fff;border-radius:999px;
    padding:0 12px;font-weight:700;font-size:.8rem;height:28px;margin-inline-end:8px}
  .btn-g{padding:9px 15px;border:1px solid var(--line);border-radius:10px;color:var(--ink);font-size:.86rem;font-weight:600;transition:.16s}
  .btn-g:hover{border-color:var(--line-hover);background:rgba(var(--wash-rgb),.05)}
  .btn-d{padding:9px 17px;border-radius:10px;background:var(--accent);color:#fff;font-size:.86rem;font-weight:700;transition:.16s}
  .btn-d:hover{background:var(--accent-2);transform:translateY(-1px)}
  .nav { display: none !important; }
  .ag-main { position: relative !important; }
  #agDeskPreviewToggle {
    position: fixed !important;
    top: 12px !important;
    right: 12px !important;
    z-index: 999999 !important;
    padding: 5px 12px !important;
    height: 32px !important;
    font-size: 0.82rem !important;
  }
  body.is-building #agDeskSidebarToggle {
    display: none !important;
    opacity: 0 !important;
    pointer-events: none !important;
  }
  @media (max-width: 900px) {
    .ag-history.on ~ #agDeskPreviewToggle,
    .ag-history.on ~ .ag-main #agDeskPreviewToggle {
      display: none !important;
    }
  }
  .ag-pv-toggle-btn:hover { border-color: #cbd5e1; background: var(--paper-2); color: var(--ink); }
  .ag-pv-toggle-btn.on {
    background: var(--card); color: var(--ink);
  }
  .ag-pv-toggle-btn .pane-right, .ag-pv-toggle-btn .pane-left {
    transition: fill 0.2s ease;
    fill: transparent;
  }
  .ag-pv-toggle-btn.on .pane-right, .ag-pv-toggle-btn.on .pane-left {
    fill: var(--accent);
  }
  .ag-pv-toggle-btn svg { width: 16px; height: 16px; flex: none; }
  /* Same [hidden]-vs-class-display trap as everywhere else in this file —
     an explicit override is needed rather than relying on the UA default. */
  .ag-pv-toggle-btn svg[hidden] { display: none; }
  @media (max-width:640px){ .nav{gap:6px;padding:11px 12px} .brand{font-size:1rem;gap:6px} }

  /* Grid before Flexbox: grid-template-columns doesn't interpolate
     between a minmax() track and a fixed one in every browser, and this
     needed to actually animate (Replit-style: a centered new-chat screen
     that resizes into the split editor+preview layout once something's
     been built, not a snap). Flex-basis on plain pixel/percent values
     transitions everywhere. */
  .ag-split{max-width:1440px;margin:0 auto;width:100%;display:flex;flex:1;min-height:0;height:calc(100vh - 56px);overflow:hidden}
  .ag-topbar{display:flex;align-items:center;width:100%;border-bottom:none;flex:none}
  .ag{text-align:left;display:flex;flex-direction:column;min-width:0;min-height:0;height:100%;max-height:100%;
    padding:0 clamp(20px,2.4vw,28px);border-right:none;
    flex:1 1 100%;transition:flex-basis .4s var(--ease);
    overflow:hidden !important}
  .ag::-webkit-scrollbar{width:0px;display:none}
  /* No build yet: .ag is the only visible column (.ag-preview collapses to
     0 below), so its own content is centred to a comfortable reading
     width instead of stretching edge-to-edge — the "new chat" screen.
     align-self:center on the column's cross axis does this without a
     second wrapper; harmless once .ag narrows to 460px too, since a
     460px column is already under the 720px cap. */
  .ag-head,.ag-log,.ag-composer,.ag-newchat-extras{width:100%;max-width:720px;align-self:center}
  .ag-split.has-preview .ag{flex:0 0 clamp(340px,34vw,460px)}
  /* The chat column flexes with the DEVICE being previewed. A phone
     mockup is only ~270px wide, so a preview pane sized for a MacBook
     left a few hundred px of empty stage next to it while the composer
     stayed cramped. These hand that width back to the chat for the phone
     and take it again for the iPad and MacBook. .ag-split gets the
     pv-* class from syncPreviewPaneSize() in the script below.
     Scoped to >900px: below that the layout is single-column tabs, so
     there are no columns to balance. */
  @media (min-width:901px){
    /* Primary: read the frame's OWN class. "has-preview" is added from
       four different places in the script, but syncPreviewPaneSize() only
       runs from two of them — so on a first preview the .pv-* class below
       was never set and the column fell back to the base width, which is
       there is nothing left to fall out of sync. */
    .ag-split.has-preview:has(.ag-pv-frame.mobile)  .ag{flex:0 0 clamp(420px,50vw,760px)}
    .ag-split.has-preview:has(.ag-pv-frame.tablet)  .ag{flex:0 0 clamp(360px,36vw,540px)}
    .ag-split.has-preview:has(.ag-pv-frame.desktop) .ag{flex:0 0 clamp(340px,28vw,430px)}
  }
  @media (min-width:901px){
    /* Fallback for engines without :has(). Kept as SEPARATE rules — an
       unsupported :has() would invalidate the whole selector list if these
       were combined with the ones above. syncPreviewPaneSize() still sets
       these classes, so this path keeps working there. */
    .ag-split.has-preview.pv-mobile  .ag{flex:0 0 clamp(420px,50vw,760px)}
    .ag-split.has-preview.pv-tablet  .ag{flex:0 0 clamp(360px,36vw,540px)}
    .ag-split.has-preview.pv-desktop .ag{flex:0 0 clamp(340px,28vw,430px)}
  }
  .ag-head{display:none}
  .ag-head .mark{width:26px;height:26px;border-radius:7px;background:var(--accent);display:grid;place-items:center}
  .ag-head .mark svg{width:15px;height:15px;color:#fff}
  .ag-head b{font-family:var(--disp);font-weight:600;font-size:1.05rem;letter-spacing:-.02em}
  .ag-head .ag-new{margin-inline-start:auto;border:1px solid var(--line);background:var(--card);border-radius:999px;
    padding:7px 15px;font-size:.84rem;font-weight:500;color:var(--ink-2);cursor:pointer;transition:.16s}
  .ag-head .ag-new:hover{border-color:var(--line-hover);color:var(--ink)}

  /* ---- conversations within one project ------------------------------
     "New build" starts a new PROJECT. This starts a new conversation in
     the one you are already in: same app, same files, same deployment,
     nothing carried over but the app itself. Three days of context is the
     reason an edit request starts getting answered in terms of a decision
     made on Monday. */
  .ag-head .ag-chats{position:relative;margin-inline-start:auto;border:1px solid var(--line);
    background:var(--card);border-radius:999px;padding:7px 12px 7px 14px;font:inherit;
    font-size:.84rem;font-weight:500;color:var(--ink-2);cursor:pointer;transition:.16s;
    display:flex;align-items:center;gap:7px;max-width:230px}
  .ag-head .ag-chats:hover{border-color:var(--line-hover);color:var(--ink)}
  .ag-head .ag-chats .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .ag-head .ag-chats svg{width:13px;height:13px;flex:none;opacity:.7}
  /* When this is showing it owns the auto margin, or both it and New build
     take one and the pair drifts apart. */
  .ag-head .ag-chats:not([hidden])~.ag-new{margin-inline-start:8px}

  .ag-chatpop{position:fixed;z-index:130;min-width:236px;max-width:320px;background:var(--card);
    border:1px solid var(--line);border-radius:13px;padding:6px;
    box-shadow:0 4px 10px -4px rgba(24,20,14,.10),0 22px 48px -20px rgba(24,20,14,.34);
    opacity:0;visibility:hidden;transform:translateY(-4px);
    transition:opacity .14s ease,transform .14s var(--ease),visibility .14s}
  :root[data-theme="dark"] .ag-chatpop{box-shadow:0 4px 10px -4px rgba(0,0,0,.5),0 24px 52px -20px rgba(0,0,0,.85)}
  .ag-chatpop.open{opacity:1;visibility:visible;transform:none}
  .ag-chatpop .ttl{font-size:.72rem;color:var(--mut);padding:5px 10px 6px}
  .ag-chatpop button{display:flex;align-items:center;gap:9px;width:100%;border:0;background:none;
    color:var(--ink);font:inherit;font-size:.85rem;text-align:left;padding:8px 10px;
    border-radius:9px;cursor:pointer}
  .ag-chatpop button:hover{background:var(--paper-2)}
  .ag-chatpop button.on{background:var(--paper-2);font-weight:600}
  .ag-chatpop button .tx{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .ag-chatpop button .n{flex:none;font-size:.74rem;color:var(--mut);font-variant-numeric:tabular-nums}
  .ag-chatpop svg{width:15px;height:15px;flex:none;color:var(--ink-2)}
  .ag-chatpop .sep{height:1px;background:var(--line);margin:5px 6px}
  .ag-chatpop .fresh{color:var(--accent)}
  .ag-chatpop .fresh svg{color:var(--accent)}

  /* The one thing in the left column that scrolls. min-height:0 is what
     lets it actually shrink inside the flex column instead of pushing the
     composer off the bottom. */
  /* scrollbar-gutter reserves the scrollbar's width on BOTH sides, and it
     is what lines the composer up with the words above it.

     The log and the composer are the same 720px box, so the composer
     centres its input inside that box — but on a platform with classic
     scrollbars the rightmost ~10px of that box is the scrollbar, not text.
     The input therefore sat flush against the scrollbar with a 10px gap on
     its left: centred on the log's border box while the messages were
     centred on its content box. It read as the composer being nudged right,
     because it was.

     `both-edges` rather than `stable`, because reserving on one side only
     moves the asymmetry into the text instead of removing it. It also stops
     the column shifting sideways the moment a conversation grows long enough
     to scroll. Where it is unsupported nothing changes, and that is fine:
     those are the platforms with overlay scrollbars, where the gutter is
     0px and there was never an offset to correct. */
  .ag-log{flex:1 1 auto;min-height:0;overflow-y:auto !important;overflow-x:hidden;scrollbar-gutter:stable both-edges;display:flex;flex-direction:column;gap:4px;padding-bottom:20px}
  /* The new-chat "hero": greeting + composer + tiles/examples centered as
     ONE group. Three earlier attempts tried to do this with
     justify-content on .ag-log alone and all of them clipped the
     greeting off the top, because those three are SIBLINGS — centering
     any one of them can't account for the others' height. This wrapper
     is the fix: it's a real box containing all three, so centering it
     centers the group, and it can't clip anything because it grows with
     its own content.

     display:contents in the default (conversation) state means the
     wrapper contributes NO layout at all there — .ag-log/.ag-composer
     behave exactly as they did before, log scrolling under a sticky
     composer. Only .ag.is-new turns it into a real flex container. */
  .ag-hero{display:contents}
  .ag.is-new .ag-hero{display:flex;flex-direction:column;justify-content:flex-start;align-items:center;padding:clamp(36px, 7vh, 80px) 0 30px;box-sizing:border-box}
  /* NEW-CHAT SCREEN DOES NOT SCROLL.
     Both the column and the log used to be scrollable here, so the
     greeting drifted under the fixed Sidebar/Preview buttons as soon as
     you touched the wheel — it should sit still until a turn starts.
     Locking both is what actually holds it: pinning the hero alone does
     nothing while .ag-log still scrolls inside it.
     That reasoning still holds for the LOG, which is what actually caused
     the drift — but it was applied to the whole section too, and the
     section is what carries Recent Projects. The result was 847px of
     content in a 720px box with overflow hidden: the strip below the fold
     could not be reached at all, at any viewport height, with no
     indication it was there.
     So the lock now sits only where it earned its place. .ag-log stays
     pinned (flex:0 0 auto, no scroll of its own) so the greeting does not
     slide while the composer is idle; the section scrolls when — and only
     when — its content genuinely overflows, which is what makes Recent
     Projects reachable. The greeting passing under the floating Sidebar /
     Preview buttons on the way is fine: those are opaque pills, not a
     transparent overlay. */
  .ag.is-new .ag-log{overflow:hidden!important;flex:0 0 auto;scrollbar-width:none!important}
  .ag.is-new{overflow-y:auto!important;overscroll-behavior:contain}
  .ag.is-new .ag-log::-webkit-scrollbar{display:none!important;width:0!important;height:0!important}
  .ag-log::-webkit-scrollbar{width:6px}
  .ag-log::-webkit-scrollbar-thumb{background:rgba(var(--wash-rgb),.14);border-radius:8px}
  .ag-log::-webkit-scrollbar-thumb:hover{background:rgba(var(--wash-rgb),.24)}

  /* The user's own message gets a distinct entrance from the agent's:
     it comes from the composer, so it rises and settles with a slight
     overshoot, while agent rows (agIn) drift in more quietly. Motion
     direction carries authorship without needing a label. */
  .ag-user{align-self:flex-end;max-width:min(86%,560px);margin:18px 0 2px;text-align:right}
  /* A QUIET SURFACE, NOT A LIT ONE.
     This was an accent gradient under an accent border under an accent-hued
     glow. One of those reads as "you said this"; three read as a button, and
     a column of them turns the transcript into a row of glowing lozenges
     competing with the reply you are actually there to read. Position and
     shape already carry authorship — right-aligned, with the corner clipped
     on the side it came from — so the colour has no work left to do. */
  .ag-user .bubble{display:inline-block;color:var(--ink);
    background:var(--paper-2);
    border:1px solid var(--line);
    border-radius:16px 16px 5px 16px;padding:12px 16px;font-size:.98rem;line-height:1.5;
    text-align:left;white-space:pre-wrap;
    transform-origin:100% 100%;animation:agSent .42s var(--ease) both}
  :root[data-theme="dark"] .ag-user .bubble{
    background:color-mix(in srgb,var(--paper-2) 82%,#fff 4%);
    border-color:rgba(255,255,255,.09)}
  .ag-user .when{display:block;font-size:.76rem;color:var(--mut);margin-top:6px;
    opacity:0;animation:agIn .3s var(--ease) .16s forwards}
  @keyframes agSent{
    from{opacity:0;transform:translateY(14px) scale(.94)}
    60%{opacity:1;transform:translateY(-2px) scale(1.01)}
    to{opacity:1;transform:none}
  }

  .ag-think{display:flex;align-items:center;gap:11px;padding:9px 2px;font-size:1rem;color:var(--ink-2);
    opacity:0;animation:agIn .35s var(--ease) forwards}
  .ag-think .ic{width:22px;height:22px;flex:none;border-radius:50%;border:1px solid var(--line);
    display:grid;place-items:center;color:var(--ink-2);background:var(--card);transition:box-shadow .25s var(--ease)}
  .ag-think .ic svg{width:13px;height:13px}
  /* Half a beat, so it comes round twice per breath and lands WITH the two
     dots instead of drifting against them. .8s was also simply faster than
     the work it reports: a spinner that outruns its own step reads as
     anxiety rather than progress.

     The halo went from a 10px blur at 45% over a 3px ring down to the ring
     alone. In a column of quiet circles the live one only has to be the lit
     one — it does not have to shout, and at 22px that much glow was mostly
     spill onto the text beside it. */
  .ag-think.busy .ic{border-color:var(--accent);border-top-color:transparent;
    animation:spin calc(var(--th-beat) / 2) linear infinite;
    box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 13%,transparent)}
  .ag-think.busy .ic svg{display:none}
  /* Was opacity-only, which read as things blinking into place. A short
     rise gives each row a direction without becoming a slideshow —
     kept to 6px and .38s so a long stage list still feels immediate. */
  @keyframes agIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
  @keyframes spin{to{transform:rotate(360deg)}}
  /* The checkmark that replaces the spinner when a stage completes —
     a small pop so finishing reads as an event, not a swap. */
  @keyframes agTick{from{transform:scale(.5);opacity:0}60%{transform:scale(1.15)}to{transform:none;opacity:1}}
  .ag-think:not(.busy) .ic svg{animation:agTick .3s var(--ease) both}
  /* Respect a system-level reduced-motion preference: keep the fades,
     drop the travel and the pop. */
  @media (prefers-reduced-motion:reduce){
    .ag-user .bubble,.ag-think,.ag-say,.ag-thinking,.ag-card,
    .ag-empty h2{animation-duration:.01ms!important;animation-delay:0s!important}
    .ag-think:not(.busy) .ic svg{animation:none}
    .ag-th-dot{animation:none!important}
    .ag-thinking:not(.done) .ag-th-label{animation:none!important}
    /* The composer bloom and rim beam keep their appearance but stop
       moving — a continuously travelling light is exactly the kind of
       thing this preference is asking us to stop. */
    .ag-box::before,.ag-box::after{animation:none!important}
    @keyframes agIn{from{opacity:0}to{opacity:1}}
    @keyframes agSent{from{opacity:0}to{opacity:1}}
  }

  /* No gutter at rest, so a finished reply starts where its text starts. */
  .ag-say{position:relative;padding:6px 2px 14px 2px;font-size:1rem;line-height:1.62;color:var(--ink);opacity:0;
    animation:agIn .35s var(--ease) forwards;
    transition:padding-left .34s var(--ease)}

  /* THE AGENT'S FACE IS ONLY THERE WHILE THERE IS SOMETHING TO SAY WITH IT.

     What stood here: a chip on EVERY reply, permanently — a dark tile
     carrying the logo, a circuit-trace mask cut from an inline SVG, a
     specular highlight, a halo, and two infinite six-second animations
     running for the life of the page on every message in the log.

     Two things wrong with it, and the cheap one is the paint.

     The real one is that a mark which is always on cannot say anything
     about now. It was drawn as a status light — it even sped up mid-turn —
     but a light that is lit before you ask, while you wait, and long after
     it is done is furniture, not a signal. And at 27px the traces, the four
     vias and the bag collapse into a smudge of dark pixels: detail that
     resolves as noise at the only size it is ever drawn.

     So the face is a STATUS now. It exists while a turn is in flight and is
     gone the moment one lands. One dot, breathing — nothing to identify,
     nothing to decode, just a sign that something is happening, which is
     the only thing this gutter was ever able to say honestly.

     Scoped to the LAST .ag-say rather than all of them:
     :not(:has(~ .ag-say)) means "no later reply exists", so the dot sits
     with the line being written instead of lighting up the whole history
     at once.

     A browser without :has() gets no dot and no gutter — which is the
     resting state exactly, so the fallback is the finished look rather
     than a broken one. */
  /* ONE TEMPO FOR THE WHOLE TURN.

     Four things used to animate while a turn was in flight and every one of
     them kept its own time: the step spinner at .8s, the header dot at 1.6,
     the label shimmer at 2.4, this dot at 1.5. Nothing lined up with
     anything, so the panel read as four separate pieces of machinery that
     happened to be switched on together — busy rather than working.

     --th-beat is the period all of them derive from now. This dot and the
     header dot breathe on the beat; the shimmer takes the same beat; the
     step spinner takes half of one, so it comes round twice per breath and
     lands with it rather than drifting against it. Change the number and
     the whole panel changes pace together, which is the point of there
     being a number. */
  :root{--th-beat:2.6s}



  /* Per-word reveal for the agent's own text (see revealWords()) — .ag-say
     itself still fades in as a block via agIn above; this staggers the
     WORDS inside it a beat apart on top of that, which is what actually
     reads as the response "arriving" rather than just appearing. Kept to
     text nodes only (revealWords walks around real <a>/<span> children),
     so a "See plans" link inside a gate message is never split mid-word. */
  .rv-w{display:inline-block;opacity:0;transform:translateY(5px);animation:agWordIn .38s var(--ease) forwards}
  @keyframes agWordIn{to{opacity:1;transform:none}}
  .ag-say b{font-weight:600}
  .ag-say .muted{color:var(--ink-2)}
  .ag-say.err{color:#b3261e}
  .ag-say.warn{color:#b06d00}

  .ag-thinking{margin:2px 0 8px;opacity:0;animation:agIn .35s var(--ease) forwards}
  .ag-th-head-row{display:flex;align-items:center;justify-content:space-between;width:100%}
  .ag-th-head{display:flex;align-items:center;gap:10px;flex:1;border:0;background:none;cursor:pointer;padding:10px 2px;font:inherit;text-align:left;color:var(--ink-2);border-radius:8px;transition:background .15s}
  .ag-interrupt-row{display:flex;align-items:center;justify-content:flex-end;width:100%;margin:2px 0 10px;opacity:0;animation:agIn .3s var(--ease) forwards}
  .ag-interrupt-btn{display:inline-flex;align-items:center;gap:6px;background:rgba(239,68,68,0.1);color:#ef4444;border:1px solid rgba(239,68,68,0.3);border-radius:999px;padding:4px 12px;font-size:.78rem;font-weight:600;cursor:pointer;transition:all .16s ease;flex:none}
  .ag-interrupt-btn:hover{background:#ef4444;color:#fff;border-color:#ef4444;box-shadow:0 4px 12px rgba(239,68,68,0.35)}
  .ag-interrupt-btn svg{width:12px;height:12px;fill:currentColor}
  .ag-queue-banner{display:flex;align-items:center;justify-content:space-between;background:rgba(59,130,246,0.12);border:1px solid rgba(59,130,246,0.3);color:var(--ink);border-radius:10px;padding:6px 14px;margin-bottom:8px;font-size:.82rem;font-weight:600}
  .ag-queue-banner button{border:0;background:none;color:#ef4444;font-size:.78rem;font-weight:700;cursor:pointer;text-decoration:underline;padding:0;margin-left:10px}
  /* Google Gemini 3-Dots Thinking Indicator (Warm Blue Edition) */
  /* Google Gemini 3-Dots Thinking Indicator (Clean Blue 3s Alternating Cycle) */
  .ag-th-gemini {
    position: relative;
    width: 26px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    margin-right: 4px;
  }

  /* Google Gemini 3-Dots Thinking Indicator (Fixed Size & Solid Warm Colors) */
  .ag-th-gemini .g-dot {
    position: absolute;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--th-dot);
    opacity: 1 !important;
  }

  /* Continuous 6s Loop: 3s Line Wave Jump -> 3s Triangle Orbit -> Repeat */
  .ag-thinking:not(.done) .g-dot-1 {
    animation: geminiCycle1 6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  }
  .ag-thinking:not(.done) .g-dot-2 {
    animation: geminiCycle2 6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  }
  .ag-thinking:not(.done) .g-dot-3 {
    animation: geminiCycle3 6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  }

  /*
     Dot 1:
     0% - 40%: Smooth vertical bounce wave in line at x = -9px (Fixed size, solid color)
     40% - 48%: Smooth glide to triangle apex (0, -7.5px)
     48% - 88%: Triangle vertex cycling / orbit (Fixed size, solid color)
     88% - 98%: Smooth glide back to line (-9px, 0)
     98% - 100%: Settled at baseline, seamless loop
  */
  @keyframes geminiCycle1 {
    0%, 16%, 32%, 40% { transform: translate3d(-9px, 0, 0); }
    5%, 21%           { transform: translate3d(-9px, -6px, 0); }
    10%, 26%          { transform: translate3d(-9px, 0, 0); }
    48%               { transform: translate3d(0, -7.5px, 0); }
    61.3%             { transform: translate3d(6.5px, 3.8px, 0); }
    74.6%             { transform: translate3d(-6.5px, 3.8px, 0); }
    88%               { transform: translate3d(0, -7.5px, 0); }
    98%, 100%         { transform: translate3d(-9px, 0, 0); }
  }

  /*
     Dot 2:
     0% - 40%: Smooth vertical bounce wave in center at x = 0 (Fixed size, solid color)
     40% - 48%: Smooth glide to bottom-right (6.5px, 3.8px)
     48% - 88%: Triangle vertex cycling / orbit (Fixed size, solid color)
     88% - 98%: Smooth glide back to center (0, 0)
     98% - 100%: Settled at baseline, seamless loop
  */
  @keyframes geminiCycle2 {
    0%, 16%, 32%, 40% { transform: translate3d(0, 0, 0); }
    3%, 19%           { transform: translate3d(0, 0, 0); }
    8%, 24%           { transform: translate3d(0, -6px, 0); }
    13%, 29%          { transform: translate3d(0, 0, 0); }
    48%               { transform: translate3d(6.5px, 3.8px, 0); }
    61.3%             { transform: translate3d(-6.5px, 3.8px, 0); }
    74.6%             { transform: translate3d(0, -7.5px, 0); }
    88%               { transform: translate3d(6.5px, 3.8px, 0); }
    98%, 100%         { transform: translate3d(0, 0, 0); }
  }

  /*
     Dot 3:
     0% - 40%: Smooth vertical bounce wave on right at x = 9px (Fixed size, solid color)
     40% - 48%: Smooth glide to bottom-left (-6.5px, 3.8px)
     48% - 88%: Triangle vertex cycling / orbit (Fixed size, solid color)
     88% - 98%: Smooth glide back to right (9px, 0)
     98% - 100%: Settled at baseline, seamless loop
  */
  @keyframes geminiCycle3 {
    0%, 16%, 32%, 40% { transform: translate3d(9px, 0, 0); }
    6%, 22%           { transform: translate3d(9px, 0, 0); }
    11%, 27%          { transform: translate3d(9px, -6px, 0); }
    16%, 32%          { transform: translate3d(9px, 0, 0); }
    48%               { transform: translate3d(-6.5px, 3.8px, 0); }
    61.3%             { transform: translate3d(0, -7.5px, 0); }
    74.6%             { transform: translate3d(6.5px, 3.8px, 0); }
    88%               { transform: translate3d(-6.5px, 3.8px, 0); }
    98%, 100%         { transform: translate3d(9px, 0, 0); }
  }

  /* Phase 3: Settled Done State */
  .ag-thinking.done .ag-th-gemini {
    animation: none !important;
    width: 14px;
  }
  .ag-thinking.done .ag-th-gemini .g-dot {
    animation: none !important;
  }
  .ag-thinking.done .ag-th-gemini .g-dot-1 {
    transform: translate3d(0, 0, 0) !important;
    background: var(--mut) !important;
    box-shadow: none !important;
    width: 6px !important;
    height: 6px !important;
    opacity: 0.6 !important;
  }
  .ag-thinking.done .ag-th-gemini .g-dot-2,
  .ag-thinking.done .ag-th-gemini .g-dot-3 {
    transform: translate3d(0, 0, 0) !important;
    opacity: 0 !important;
    box-shadow: none !important;
    display: none !important;
  }

  /* Freeze all child spinners and stage icons when thinking completes or stops */
  .ag-thinking.done .ag-think .ic,
  .ag-thinking.done .ag-think.busy .ic {
    animation: none !important;
    border-color: var(--line) !important;
    box-shadow: none !important;
  }
  .ag-thinking.done .ag-think.busy .ic svg {
    display: block !important;
  }

  .ag-th-dot { display: none !important; }

  @media (prefers-reduced-motion: reduce) {
    .ag-th-gemini,
    .ag-th-gemini .g-dot,
    .ag-thinking:not(.done) .g-dot-1,
    .ag-thinking:not(.done) .g-dot-2,
    .ag-thinking:not(.done) .g-dot-3 {
      animation: none !important;
    }
  }
  .ag-th-label{font-size:1.08rem;font-weight:600;color:var(--ink);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:min(60vw,420px)}
  /* A light travelling across the words while the turn is live. Guarded by
     @supports because the effect works by making the text itself
     transparent and painting a gradient through it - where background-clip
     does not apply, that is an invisible label rather than a plain one. */
  @supports ((-webkit-background-clip:text) or (background-clip:text)){
    .ag-thinking:not(.done) .ag-th-label{
      background-image:linear-gradient(100deg,
        var(--ink-2) 30%, var(--ink) 45%, var(--ink) 55%, var(--ink-2) 70%);
      background-size:250% 100%;
      -webkit-background-clip:text;background-clip:text;
      -webkit-text-fill-color:transparent;color:transparent;
      animation:thShimmer var(--th-beat) linear infinite}
  }
  @keyframes thShimmer{from{background-position:200% 0}to{background-position:-50% 0}}

  /* A file the model wrote. Monospace because it is a path, and dimmer than
     a stage name because a list of twelve should read as one block of work
     rather than twelve announcements. */
  .ag-th-body .ag-think.is-file{padding:3px 2px;font-size:.85rem;color:var(--mut);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
  .ag-th-body .ag-think.is-file .ic{width:16px;height:16px;border:0;background:none}
  .ag-th-body .ag-think.is-file .ic svg{width:11px;height:11px;color:var(--ok)}
  .ag-th-time{font-size:.85rem;color:var(--mut);font-weight:400}
  .ag-th-chev{width:14px;height:14px;color:var(--mut);margin-inline-start:auto;flex:none;transition:transform .2s var(--ease)}
  .ag-thinking.collapsed .ag-th-chev{transform:rotate(-90deg)}
  /* Body is now a real, possibly-long step log (one .ag-think row per
     stage), not one cosmetic line — sized to fit a full pipeline, with
     its own scroll for an unusually long repair sequence, and still
     collapses to 0 for the "Thought for Ns" summary state. */
  .ag-th-body{padding:0 2px 4px 4px;max-height:480px;overflow-y:auto;overflow-x:hidden;
    transition:max-height .3s var(--ease),opacity .2s,padding .2s}
  /* This was the one scrollable region in the page with no scrollbar of its
     own, so it fell through to the platform default — a full-width track
     with arrow buttons, sitting inside a message stream where .ag-log and
     .ag-hist-view are a thin tinted thumb on no track. Same rules as those,
     and --wash-rgb is the scrollbar ink, already flipped per theme.

     Deliberately NO scrollbar-width/scrollbar-color here. Chromium honours
     the standard properties over ::-webkit-scrollbar, so measured in this
     browser: these rules alone give the 6px this asks for, adding
     scrollbar-width:thin widens it to 10px, and scrollbar-color WITHOUT a
     width falls all the way back to the 15px default — wider than doing
     nothing was supposed to fix. Firefox keeps its own thin overlay bar,
     exactly as it already does for every other scroller on this page. */
  .ag-th-body::-webkit-scrollbar{width:6px}
  .ag-th-body::-webkit-scrollbar-track{background:transparent}
  .ag-th-body::-webkit-scrollbar-thumb{background:rgba(var(--wash-rgb),.14);border-radius:8px}
  .ag-th-body::-webkit-scrollbar-thumb:hover{background:rgba(var(--wash-rgb),.24)}
  .ag-thinking.collapsed .ag-th-body{max-height:0;opacity:0;padding-top:0;padding-bottom:0}
  .ag-th-body .ag-think{padding:6px 2px;font-size:.93rem}

  /* ONE THREAD, NOT A COLUMN OF RINGS.

     Nine steps drew nine isolated circles, so the eye got nine objects
     instead of one process — nothing on screen said the third thing
     followed the second, and the file rows in between read as a different
     widget that had wandered in. A 1px rail behind the icons is the whole
     fix: the circles stop being a list and become stations on something
     continuous.

     The rail spans the FULL height of every row and the icons are opaque,
     so each one punches its own gap in it. No per-row segment maths and
     nothing to recompute when a step arrives mid-turn — a row added to the
     middle brings its own piece of thread with it. First and last rows are
     half-length, so the line begins and ends AT a node instead of running
     past one into nothing.

     File rows get a 22px icon BOX like every other row — the tick inside
     stays 11px — purely so their centre sits on the same axis. Two centres
     is what would turn the rail into a zigzag, and it is the only reason
     the box is being resized at all. Their disc is --paper rather than
     --card, so it reads as the thread passing behind a small mark instead
     of another chip competing with the step circles above it. */
  .ag-th-body .ag-think{position:relative}
  .ag-th-body .ag-think::before{
    content:"";position:absolute;left:12px;top:0;bottom:0;width:1px;
    background:var(--line);pointer-events:none}
  .ag-th-body .ag-think:first-child::before{top:50%}
  .ag-th-body .ag-think:last-child::before{bottom:50%}
  .ag-th-body .ag-think .ic{position:relative;z-index:1}
  .ag-th-body .ag-think.is-file .ic{width:22px;height:22px;background:var(--paper)}

  .ag-worked{display:flex;align-items:center;gap:11px;padding:4px 2px 18px;font-size:.92rem;color:var(--mut);
    opacity:0;animation:agIn .35s var(--ease) forwards}
  .ag-worked .ic{width:22px;height:22px;flex:none;border-radius:50%;border:1px solid var(--line);display:grid;place-items:center;background:var(--card)}
  .ag-worked .ic svg{width:12px;height:12px}

  /* Sized to the list, not to the column. It was a full-width card with
     roomy rows, so ten files became a slab bigger than the message that
     explained them — and the list is a receipt, not the point of the turn.
     max-content keeps it exactly as wide as its longest path. */
  .ag-card{margin:0 0 16px 35px;background:var(--card);border:1px solid var(--line);
    border-radius:12px;padding:4px 4px 5px;
    width:max-content;max-width:100%}
  /* ONE ROW PER FILE, WITH WHAT IT COST.
     These were pills carrying a path and nothing else, so a one-word copy
     fix and a rewritten component were the same object on screen — a wrapped
     hedge of identical grey lozenges you had to read word by word to find
     anything in. A build is a list of changes, and the thing that makes a
     list of changes scannable is the number beside each line. */
  .ag-card .files{display:flex;flex-direction:column;gap:0;margin:0 0 4px}
  .ag-file{display:flex;align-items:center;gap:14px;padding:1px 8px;border-radius:6px;
    font-size:.7rem;line-height:1.62;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    min-width:0}
  .ag-file:hover{background:var(--paper-2)}
  /* The directory is context and the filename is the answer, so they are not
     the same colour. At a glance you read a column of filenames; the paths
     are there when you need to tell two Header.tsx apart. */
  .ag-file .p{min-width:0;flex:1 1 auto;overflow-wrap:anywhere;color:var(--mut)}
  .ag-file .p b{font-weight:600;color:var(--ink-2)}
  /* Fixed width and tabular figures so the counts form a column that can be
     compared down the list rather than a ragged edge that cannot. */
  .ag-file .d{flex:none;display:flex;justify-content:flex-end;gap:6px;min-width:62px;
    font-variant-numeric:tabular-nums;font-size:.685rem;letter-spacing:-.01em}
  .ag-file .d i{font-style:normal;opacity:.92}
  .ag-file .add{color:var(--diff-add)}
  .ag-file .del{color:var(--diff-del)}
  /* A new file has nothing to compare against, so a "+N / -0" would be
     arithmetic pretending to be information. It gets a word instead. */
  /* A dot, not a badge. "NEW" in a green pill was the loudest thing in the
     card, which is backwards — being new is the least surprising fact about
     a file in a build that just created most of them. */
  .ag-file .new{flex:none;width:5px;height:5px;border-radius:50%;
    background:var(--diff-add);opacity:.75}
  .ag-card .cost{font-size:.69rem;color:var(--mut);padding:4px 8px 1px;border-top:1px solid var(--line);margin-top:3px}
  /* Green and red that survive both themes. The light pair is darkened well
     past the usual UI green so it clears contrast on the card, and the dark
     pair is lifted for the same reason in the other direction — a diff you
     cannot read is decoration. */
  :root{--diff-add:#15803d;--diff-del:#be123c}
  :root[data-theme="dark"]{--diff-add:#4ade80;--diff-del:#fb7185}
  @media (prefers-color-scheme:dark){
    :root:not([data-theme="light"]){--diff-add:#4ade80;--diff-del:#fb7185}
  }

  ::-webkit-scrollbar-corner{background:transparent !important}
  .ag-composer{flex:0 0 auto;position:relative;bottom:0;z-index:10;background:var(--paper);padding:8px 0 calc(12px + env(safe-area-inset-bottom));overflow:visible}
  /* Inset by the log's own scrollbar gutter, measured at runtime and
     published as --log-gutter (see publishLogGutter in code.html).

     The log reserves that gutter on both edges and the composer did not, so
     the card was 6px wider on each side than the messages and the scroll
     thumb travelled underneath its rounded corner. Falls back to 0, which is
     correct on overlay-scrollbar platforms where there is no gutter to match. */
  .ag-composer{padding-inline:var(--log-gutter, 0px)}
  /* Lifted off the page for the length of the travel: the shadow grows as it
     leaves the middle of the screen and settles as it lands, so the move
     reads as the composer being placed rather than teleporting. will-change
     keeps it on its own compositor layer for the half second it is moving —
     and only then, because a permanent layer on a full-width bar costs memory
     for the 99% of the time it is still. */
  /* A clone of what the new-chat screen was, dissolving upward while the
     composer goes down. pointer-events:none because it is a picture of
     something that has already gone. */
  .ag-hero-ghost{
    pointer-events:none;z-index:6;opacity:1;
    transform:translateY(0) scale(1);transform-origin:50% 50%;
    /* opacity and transform only — both composited. The blur that was here
       ran a filter over three full-width clones every frame, which is the
       most expensive thing a phone could have been asked to do during the
       one moment the composer needs a clear 60fps. A slightly larger rise
       and a scale down read as depth without it. */
    transition:opacity .44s ease-out,
               transform .66s cubic-bezier(.32,.72,0,1)}
  .ag-hero-ghost.out{opacity:0;transform:translateY(-30px) scale(.965)}
  /* Staggered: the greeting goes first, the tiles behind it, the examples
     last. Three things leaving on the same frame is a flicker; leaving in
     the order you read them is a movement. */
  .ag-hero-ghost:nth-of-type(2){transition-delay:.05s}
  .ag-hero-ghost:nth-of-type(3){transition-delay:.10s}

  /* The first line of the conversation, arriving under the composer tail. */
  @keyframes agFirstIn{
    from{opacity:0;transform:translateY(16px)}
    to{opacity:1;transform:none}
  }
  /* Same curve and a delay that puts it mid-flight, so it is already settling
     as the composer lands rather than starting afterwards. No blur here
     either: it sits directly behind the bar that has to stay smooth. */
  .ag-user.ag-first-in{animation:agFirstIn .5s cubic-bezier(.32,.72,0,1) .14s both}
  @media (prefers-reduced-motion:reduce){
    .ag-hero-ghost{display:none}
    .ag-user.ag-first-in{animation:none}
  }

  .ag-composer.ag-flipping{
    will-change:transform;
    /* The shadow is STATIC for the length of the flight, and the transition
       lists transform only.

       It used to animate box-shadow alongside it, and a moving shadow is a
       repaint of the blurred area on every single frame — the one thing in
       here that could not run on the compositor, and the reason a 0.5s move
       could drop frames on a phone. Painted once into the layer that
       will-change already creates, it costs nothing to carry along.

       The curve is gentler than the expo-out it replaces: that one spent
       most of its distance in the first few frames and then crawled, which
       reads as a lurch followed by a wait rather than as one movement. */
    box-shadow:0 22px 50px -26px rgba(0,0,0,.45);
    transition:transform .62s cubic-bezier(.32,.72,0,1)}
  .ag-composer.ag-flipping .ag-box{
    /* The glow the box carries is also a shadow; holding it still while the
       bar travels keeps the whole thing to one composited layer. */
    transition:none !important}
  @media (prefers-reduced-motion:reduce){
    .ag-composer.ag-flipping{transition:none;box-shadow:none}
  }

  /* ═══════ THE BAR FLOATS OVER THE CONVERSATION ═══════
     A solid bar is a floor: the log stops dead at its top edge and the
     conversation ends in a hard horizontal line, which is why the last
     message always looked like the end of the page rather than the
     newest thing said. The bar is frosted glass instead — the log runs
     underneath it and you can read the message passing behind while you
     scroll, the way a phone keyboard bar or Claude's own composer reads.

     Three parts, and all three are needed:
       - the log extends BEHIND the bar, so there is something to see
         through it (the two positioning rules below);
       - the bar's own fill is part paper, part nothing, over a
         backdrop-filter — the blur is what keeps text legible on top of
         moving content instead of turning the bar into a window;
       - ::before feathers the top edge, because a backdrop-filter has a
         hard boundary and a line of text crossing it snaps from sharp to
         blurred. The gradient gives it 38px to dissolve in.

     Only with a conversation on screen. The new-chat screen centres this
     same form in the middle of the hero with nothing behind it, where
     glass has nothing to be glass over. */
  .ag:not(.is-new) .ag-composer,
  .ag.has-convo .ag-composer,
  .ag:has(#agLog > *:not(.ag-empty)) .ag-composer{
    /* THE BAND IS NOT THE GLASS ANY MORE — THE BOX IS.

       This used to be a frosted bar with an opaque card sitting on it, and
       the card is the part you actually look at. So the log dissolved into
       the band around the input and then hit a hard white rectangle, which
       is the "it looks bugged" of it: the one element with a visible edge
       was the one element you could not see through.

       Moving the blur onto .ag-box also removes a nested backdrop-filter.
       An element with backdrop-filter becomes a backdrop root, so a child
       that also has one filters its parent's output rather than the page —
       the inner blur was doing nothing dependable. There is exactly one
       blurring surface now, and it is the one with the rounded edge. */
    background:transparent;
    -webkit-backdrop-filter:none;
    backdrop-filter:none;
    border-top:0;
  }
  /* The top feather went with the band. It existed to dissolve a hard
     backdrop-filter boundary that no longer spans the column — the box has
     its own rounded edge and its own falloff, and a gradient above a
     floating card just dims the message behind it for no reason. */

  /* The glass itself. Only when docked: the new-chat hero centres this same
     box over empty paper, where transparency has nothing to reveal and the
     aurora behind it is the whole point of the screen. */
  .ag:not(.is-new) .ag-box,
  .ag.has-convo .ag-box,
  .ag:has(#agLog > *:not(.ag-empty)) .ag-box{
    /* 54% fill over a 16px blur, and the blur being SMALLER is what makes
       the effect read. At 24px a sentence passing underneath dissolves into
       an even wash — technically transparent, visibly nothing. At 16 the
       line keeps its shape: you can see words travelling under the glass,
       which is the whole point of putting glass there.
       54 is the floor. Below it the placeholder starts competing with the
       text behind it, which costs legibility for no extra effect. */
    background:color-mix(in srgb,var(--paper) 54%,transparent);
    -webkit-backdrop-filter:blur(16px) saturate(1.8);
    backdrop-filter:blur(16px) saturate(1.8);
    border:1px solid rgba(255,255,255,.7);
    /* Lighting, in the order it reads:
         1px hairline   — a defined edge against moving text, which a blur
                          alone never gives you;
         the two drops  — lift. Without them the box sits IN the text
                          rather than over it, and the blur reads as a
                          smudge on the page instead of a pane above it;
         the cyan       — what is left of the aurora. Outer box-shadows are
                          clipped to outside the border box, so this cannot
                          bleed through a translucent fill the way the
                          ::before bloom does;
         inset top      — specular. The line where light catches the near
                          edge of a piece of glass. */
    box-shadow:
      0 0 0 1px rgba(23,23,23,.055),
      0 12px 32px -12px rgba(23,23,23,.16),
      0 2px 8px -4px rgba(23,23,23,.10),
      0 0 28px -6px rgba(var(--glow-a),.16),
      inset 0 1px 0 rgba(255,255,255,.85),
      inset 0 -1px 0 rgba(23,23,23,.03);
  }
  /* The bloom is a lamp behind an opaque card. Behind a translucent one it
     is a coloured stain on the words underneath — ::before sits at
     z-index:-1, so the moment the fill stops being opaque the gradient
     paints straight through it. The hero keeps it; docked, the rim beam
     (::after) and the cyan drop above carry the colour instead. */
  .ag:not(.is-new) .ag-box::before,
  .ag.has-convo .ag-box::before,
  .ag:has(#agLog > *:not(.ag-empty)) .ag-box::before{
    opacity:0;
    animation:none;
  }
  .ag:not(.is-new) .ag-box:focus-within,
  .ag.has-convo .ag-box:focus-within,
  .ag:has(#agLog > *:not(.ag-empty)) .ag-box:focus-within{
    /* Focus thickens the light, not the geometry — same rule the hero
       state already follows, so nothing shifts under the cursor. */
    background:color-mix(in srgb,var(--paper) 64%,transparent);
    box-shadow:
      0 0 0 1px rgba(23,23,23,.07),
      0 16px 40px -14px rgba(23,23,23,.20),
      0 2px 8px -4px rgba(23,23,23,.12),
      0 0 34px -6px rgba(var(--glow-a),.26),
      inset 0 1px 0 rgba(255,255,255,.9),
      inset 0 -1px 0 rgba(23,23,23,.03);
  }

  /* Dark glass is not light glass with a dark fill. The specular rim has to
     drop to a hint — a 85%-white line on a dark pane reads as a rendering
     seam, the same reason the hero's dark rule already kills it — and the
     drop shadows have to go nearly black, because a grey shadow over a dark
     page is invisible and the box loses its lift entirely. */
  :root[data-theme="dark"] .ag:not(.is-new) .ag-box,
  :root[data-theme="dark"] .ag.has-convo .ag-box,
  :root[data-theme="dark"] .ag:has(#agLog > *:not(.ag-empty)) .ag-box{
    background:color-mix(in srgb,#25262b 62%,transparent);
    border:1px solid rgba(255,255,255,.10);
    box-shadow:
      0 0 0 1px rgba(0,0,0,.35),
      0 14px 36px -12px rgba(0,0,0,.62),
      0 2px 10px -4px rgba(0,0,0,.45),
      0 0 32px -6px rgba(var(--glow-a),.24),
      inset 0 1px 0 rgba(255,255,255,.07);
  }
  :root[data-theme="dark"] .ag:not(.is-new) .ag-box:focus-within,
  :root[data-theme="dark"] .ag.has-convo .ag-box:focus-within,
  :root[data-theme="dark"] .ag:has(#agLog > *:not(.ag-empty)) .ag-box:focus-within{
    background:color-mix(in srgb,#25262b 72%,transparent);
    box-shadow:
      0 0 0 1px rgba(0,0,0,.4),
      0 18px 44px -14px rgba(0,0,0,.7),
      0 2px 10px -4px rgba(0,0,0,.5),
      0 0 38px -6px rgba(var(--glow-a),.36),
      inset 0 1px 0 rgba(255,255,255,.09);
  }

  /* No backdrop-filter support (older Firefox with it disabled, some
     embedded webviews): a 66%-transparent bar over live text is unreadable
     without the blur, so those browsers get an opaque one. Progressive
     enhancement rather than a fallback that ships broken. */
  @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
    .ag:not(.is-new) .ag-box,
    .ag.has-convo .ag-box,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-box{
      background:var(--card);
    }
    :root[data-theme="dark"] .ag:not(.is-new) .ag-box,
    :root[data-theme="dark"] .ag.has-convo .ag-box,
    :root[data-theme="dark"] .ag:has(#agLog > *:not(.ag-empty)) .ag-box{
      background:#25262b;
    }
  }

  /* Above 900 the bar is a flex item at the end of the column, so the log
     can only ever stop where it starts. Taking it out of flow is what
     lets the log own the full height and scroll behind it — absolute
     within .ag rather than fixed to the viewport, because at this width
     .ag is one column of a split and a fixed bar would sit across the
     preview too.

     inset-inline repeats .ag's own side padding so the bar lines up with
     the messages instead of the column edges, and width:auto is
     load-bearing for the same reason it is in the phone rule below: the
     shared width:100% would resolve against the padding box and hang the
     send button off the right edge. */
  @media (min-width:901px){
    .ag{position:relative}
    .ag:not(.is-new) .ag-composer,
    .ag.has-convo .ag-composer,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-composer{
      position:absolute;inset-inline:clamp(20px,2.4vw,28px);
      /* Off the floor, not on it. The gap underneath is a strip of the
         live log — text scrolls through it in the clear, under a bar that
         is no longer the last thing on the screen. */
      bottom:12px;top:auto;width:auto;z-index:40;
      /* Over-constrained on purpose: left, right AND a 720px max-width are
         all set, and the rule that resolves that drops `right` — which
         left-aligned the bar against the column edge while the messages
         above it stayed centred, 176px to its right. Auto margins split
         the leftover instead, which is what centres it on them. */
      margin-inline:auto;
    }
    /* The bar's measured height, so the last message can be scrolled out
       from under it. --composer-h is kept current by the ResizeObserver
       further down; the fallback only matters for the first paint. */
    .ag:not(.is-new) #agLog,
    .ag.has-convo #agLog,
    .ag:has(#agLog > *:not(.ag-empty)) #agLog{
      padding-bottom:calc(var(--composer-h, 110px) + 26px);
    }
  }
  /* ---------------------------------------------------------------
     Composer treatment: an aurora bloom (::before) behind the box and a
     travelling light beam (::after) around its rim.

     WHY IT'S BUILT THIS WAY. .ag clips horizontally and always will (see
     the comment on the .ag rule above), so the sideways fade budget is
     just .ag's 20px padding floor. filter:blur(N) needs roughly 2N of
     clear space to fall off to nothing, so a big blur and a big sideways
     bloom are simply not co-satisfiable here — every previous attempt to
     get one produced a flat vertical cut where the bloom met the clip.

     So the drama lives on the two axes that AREN'T clipped:
       - vertically, where .ag scrolls rather than hides and there's real
         flow space, the bloom goes big (-70px);
       - around the rim, where the beam sits entirely INSIDE the box's own
         bounds and therefore costs zero clip budget and can never be cut.
     Sideways, the bloom is deliberately restrained and its gradients are
     tuned to reach transparent on their own, so the safety mask feathers
     an already-dark band instead of slicing a bright one. A mask only
     looks like a cut when it eats colour that was still visible.

     Palette lives in custom properties so Power Agent mode is a PALETTE
     SWAP, not a second copy of the geometry — same idiom as home.html's
     scroll showcase (@property --c + color-mix layered gradients). Every
     inset, radius, blur and duration below is written exactly once. */
  .ag-box{
    /* rgb triplets so alpha can vary per layer via rgba(var(--x),a) */
    /* Cyan -> violet -> PINK. The third stop used to be a deep blue,
       which put every hue in the ring inside one quarter of the colour
       wheel: the result read as a single soft blue wash rather than a
       spectrum, and no amount of opacity fixed that, because the problem
       was hue spread and not level. */
    --glow-a:34,211,238;   /* cyan   */
    --glow-b:139,92,246;   /* violet */
    --glow-c:236,72,153;   /* pink   */
    --beam-a:34,211,238;
    --beam-b:236,72,153;
    /* Two independent multipliers, composed in ::before's opacity below.
       --glow-alpha is the THEME's overall level, --glow-focus is the
       focus intensification. Keeping them separate means focusing in dark
       mode brightens relative to the dark level instead of jumping back
       to the light one. */
    --glow-alpha:1;
    --glow-focus:1;
    position:relative;background:var(--card);border:1px solid var(--line);border-radius:16px;
    padding:12px 14px 10px;
    /* The two coloured shadows ARE the side glow. ::before below sits at
       z-index:-1 and therefore paints underneath this element's opaque
       var(--card) background — the only part of it that shows horizontally
       is the thin strip outside the box, and a centre-out radial gradient
       is by construction DIMMEST there. That mismatch is why every attempt
       to get a side glow out of the bloom came back flat. A box-shadow is
       the right primitive for this: its silhouette is the box itself, so
       it is brightest exactly at the edge and fades outward.
       Reach is blur/2 + spread, kept inside .ag's 20px padding budget:
       34/2-2 = 15px. */
    box-shadow:
      0 0 34px -2px rgba(var(--glow-a),.22),
      0 0 12px -3px rgba(var(--glow-b),.18),
      0 1px 3px rgba(0,0,0,.08),
      inset 0 1px 0 rgba(255,255,255,.55);
    transition:border-color .2s,box-shadow .2s}
  .ag-box.power-mode{
    --glow-a:225,29,72;    /* crimson    */
    --glow-b:251,113,133;  /* rose       */
    --glow-c:159,18,57;    /* deep wine  */
    --beam-a:225,29,72;
    --beam-b:251,113,133;
  }
  /* The bloom used to be pulled back to .62 here, because a cyan/violet
     wash over the WARM near-black paper (#1c1917) reads hotter than the
     same wash on light paper. That was right for a wash and wrong now:
     the surface below is neutral rather than warm, so the hues stop
     fighting the paper under them and the ring can run at full strength,
     which is the entire point of having one.

     The white inner rim still has to go. On a dark card a 55%-white top
     edge looks like a rendering seam. */
  :root[data-theme="dark"] .ag-box{
    --glow-alpha:1;
    /* Neutral, and a step lighter than --card's warm #2a2622. Warm brown
       under a cyan ring muddies it toward grey; a neutral surface lets
       the colour read as the colour it is. */
    background:#2b2a33;
    border-color:rgba(255,255,255,.09);
    box-shadow:
      0 0 40px -2px rgba(var(--glow-a),.40),
      0 0 18px -4px rgba(var(--glow-b),.34),
      0 0 26px -6px rgba(var(--glow-c),.30),
      0 1px 3px rgba(0,0,0,.45),
      inset 0 1px 0 rgba(255,255,255,.07);
  }
  :root[data-theme="dark"] .ag-box:focus-within{
    box-shadow:
      0 0 52px -2px rgba(var(--glow-a),.55),
      0 0 22px -4px rgba(var(--glow-b),.44),
      0 0 30px -6px rgba(var(--glow-c),.40),
      0 4px 14px rgba(var(--glow-a),.22),
      inset 0 1px 0 rgba(255,255,255,.09);
  }
  /* 44/2-2 = 20px reach, exactly the padding budget — the halo brightens
     and widens to the limit without ever reaching the clip line. */
  .ag-box:focus-within{--glow-focus:1.35;border-color:rgb(var(--beam-a));
    box-shadow:
      0 0 44px -2px rgba(var(--glow-a),.34),
      0 0 16px -3px rgba(var(--glow-b),.26),
      0 4px 12px rgba(var(--glow-a),.12),
      inset 0 1px 0 rgba(255,255,255,.55)}

  /* Layer 1 — the aurora bloom. */
  .ag-box::before{
    content:"";position:absolute;z-index:-1;pointer-events:none;
    /* Vertical is free (unclipped) so it carries the size. Horizontal
       stays under .ag's 20px padding floor with room to spare, because
       the blur below still smears outward from wherever colour ends. */
    /* Top bleed is nearly free: negative block-start overflow is
       unreachable, so it adds no scroll length to .ag. The bottom is kept
       shorter than the top because that direction DOES extend .ag's
       scroller — but not so short that the glow reads as lit only from
       above, which looked lopsided rather than ambient. */
    inset:-72px -12px -44px;border-radius:44px;
    /* THE STOPS ARE ARITHMETIC, NOT TASTE. For a radial-gradient the two
       percentages are radii measured against the element's full width and
       full height, so a radius of r with its last stop at s reaches
         reach = |2(c - .5)| + 2·r·s
       in half-axis units. reach >= 1 means the gradient is STILL painting
       colour when it hits the element's own edge — which is a dead
       straight line that blur only softens, never removes. That is what
       the previous `46% 112% ... transparent 72%` did: 2(1.12)(.72) =
       1.61, i.e. it ran off the top and bottom edge at ~25% alpha and
       produced exactly the hard band above and below the box.
       Every layer below is verified <= .92:
         L1 vertical    2(.62)(.72) = .89
         L1 horizontal  2(.46)(.72) = .66
         L2 vertical    .12 + 2(.58)(.68) = .91
         L2 horizontal  .16 + 2(.40)(.68) = .70
       Keep the two hues spatially apart, too — stacked on one centre they
       average through the blur into a flat lavender-grey and stop reading
       as light at all. */
    background:
      radial-gradient(46% 62% at 56% 52%, rgba(var(--glow-a),.66) 0%, rgba(var(--glow-c),.32) 46%, transparent 72%),
      radial-gradient(40% 58% at 34% 46%, rgba(var(--glow-b),.40) 0%, transparent 68%);
    filter:blur(22px);
    opacity:calc(.95 * var(--glow-alpha) * var(--glow-focus));
    will-change:transform;
    animation:agGlowMove 9s ease-in-out infinite alternate;
    transition:opacity .35s var(--ease);
    /* NO MASK, deliberately. Three separate attempts here (linear 18/82,
       linear 4/96, then an ellipse) all failed the same way: a mask is a
       symptom-hider, and any mask tight enough to catch a still-bright
       gradient is also tight enough to flatten the bloom into a disc — or,
       if it is linear, to replace the cut with a different dead-straight
       edge of its own. With the reach math above the gradients terminate
       on their own and there is nothing left to feather. */
  }
  /* Focus intensifies the LIGHT, never the geometry — --glow-focus is set
     on .ag-box:focus-within above and inherits down to here. The old rule
     scaled the bloom up by 1.08, which pushed it past the bleed budget
     and into the clip: the focus state was itself causing a cut. */
  /* Drift is vertical-dominant for the same budget reason: sideways
     travel spends the one axis that has none to spare. */
  @keyframes agGlowMove{
    0%{transform:translate3d(0,0,0) scaleY(1)}
    50%{transform:translate3d(-2px,7px,0) scaleY(1.06)}
    100%{transform:translate3d(2px,-6px,0) scaleY(.96)}
  }

  /* Layer 2 — the travelling rim beam. A registered custom property is
     what makes a conic gradient animatable at all (plain custom props
     interpolate as strings and just snap); same technique as home.html.
     Where @property is unsupported the angle simply stays at 0deg and the
     ring renders as a static gradient border — degraded, not broken. */
  @property --ag-beam-angle{syntax:'<angle>';inherits:false;initial-value:0deg}
  .ag-box::after{
    content:"";position:absolute;inset:-1px;border-radius:17px;
    padding:1.5px;pointer-events:none;
    background:conic-gradient(from var(--ag-beam-angle) at 50% 50%,
      transparent 0deg,transparent 186deg,
      rgba(var(--beam-b),.85) 246deg,
      rgba(var(--beam-a),1) 300deg,
      rgba(var(--beam-b),.5) 330deg,
      transparent 360deg);
    /* Standard gradient-border trick: paint the full rect, then subtract
       the content box so only the 1.5px ring survives. The children
       (textarea/.ag-brow) are unpositioned so this paints above them, but
       the ring only ever occupies the rim and takes no pointer events. */
    /* Order matters: the `mask` shorthand resets mask-composite, so each
       composite declaration has to follow its own shorthand. */
    -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
    -webkit-mask-composite:xor;
    mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
    mask-composite:exclude;
    opacity:.7;
    animation:agBeamSpin 7s linear infinite;
    transition:opacity .35s var(--ease);
  }
  .ag-box:focus-within::after{opacity:1;animation-duration:3.4s}
  @keyframes agBeamSpin{to{--ag-beam-angle:360deg}}

  /* Power Agent Mode's crimson aura is the glow/beam palette override on
     .ag-box.power-mode above — geometry is shared, so there is nothing
     else to declare here. The old version duplicated every inset, blur
     and radius, which is how it drifted into three separate bugs: its
     -28px/-36px horizontal insets were double the clip budget and got
     hard-cut; it never reset the base mask, so the base mask silently
     applied to it anyway; and its keyframes animated `filter`, which
     overrode the blur its own :focus-within rule was trying to set (and
     animating filter is not compositor-friendly to begin with).
     agPowerPulse / agOuterPowerPulse / agPowerBorderPulse all went with
     it — the rim beam now carries the "armed" read that the border pulse
     used to. */
  .ag-box textarea{width:100%;border:0;outline:0;resize:none;background:none;font-family:var(--body);font-size:.95rem;line-height:1.5;color:var(--ink);min-height:24px;max-height:140px;overflow-y:auto;padding:0}
  .ag-box textarea::placeholder{color:var(--mut);font-weight:400}
  /* wrap, because .ag-attach-row below asks for a whole line and cannot get
     one without it. In a nowrap row its width:100% is just a shrink target:
     the chips ended up as a 419px block sitting BESIDE the + button, sharing
     the line with every control, and each extra image made the strip taller
     and pushed the buttons down instead of stacking above them. With nothing
     attached the row still fits on one line, so this changes nothing until
     there is something to show. */
  .ag-brow{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding-top:6px}
  .ag-brow-sp{flex:1}

  /* Attach button + the chip that replaces it once a file's picked —
     Replit's "+" in the composer row, repurposed here specifically for a
     logo/image the agent should build from rather than invent. Rectangular
     like every other control here (see the sitewide button pass), not the
     old circular .ag-send. */
  .ag-attach{flex:none;width:30px;height:30px;border:1px solid var(--line);background:transparent;color:var(--ink-2);
    border-radius:6px;display:grid;place-items:center;cursor:pointer;transition:.15s}
  .ag-attach:hover{border-color:var(--line-hover);color:var(--ink)}
  .ag-attach svg{width:16px;height:16px}
  /* Chips wrap onto their own line rather than squeezing the composer's
     button row — eight attachments is a legitimate thing to do and must not
     push the send button off the end. */
  /* Thumbnails on a message that carried photos. Fixed aspect and cover, so
     a portrait photo beside a landscape one cannot make the row ragged. */
  .ag-user-imgs{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end;margin-bottom:6px}
  .ag-user-imgs img{width:54px;height:54px;border-radius:9px;object-fit:cover;border:1px solid var(--line);background:var(--paper-2)}
  /* Dragging a file over the composer. A visible target, because a drop that
     misses navigates the page to the file and loses the conversation. */
  .ag-composer.dropping .ag-box{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent)}
  .ag-attach-row{display:contents}
  .ag-attach-row:not(:empty){display:flex;flex-wrap:wrap;gap:6px;order:-1;width:100%}
  /* Uploading: dimmed, and the remove button still works — a slow upload is
     the moment you are most likely to change your mind. */
  .ag-attach-chip.busy{opacity:.6}
  .ag-attach-chip.busy img{filter:grayscale(1)}
  .ag-attach-chip.failed{border-color:#d9382c;color:#d9382c}
  .ag-attach-chip.failed img{opacity:.4}
  .ag-attach-chip[hidden]{display:none}
  .ag-attach-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:var(--paper-2);
    border-radius:999px;padding:3px 4px 3px 3px;font-size:.78rem;font-weight:600;color:var(--ink-2);max-width:160px}
  .ag-attach-chip img{width:20px;height:20px;border-radius:5px;object-fit:cover;flex:none;background:#fff}
  .ag-attach-chip .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .ag-attach-chip button{flex:none;border:0;background:none;color:var(--mut);cursor:pointer;width:16px;height:16px;display:grid;place-items:center}
  .ag-attach-chip button:hover{color:var(--ink)}
  .ag-attach-chip button svg{width:12px;height:12px}

  /* Claude & Replit Model Selector Pill */
  /* ── EFFORT ───────────────────────────────────────────────────────────
     How hard to think, as one value on a range rather than a mode you had
     to know to pair with something else. Reads as a value, not a menu: no
     chevron, because it opens a slider and not a list. */
  .ag-effort-wrap{position:relative;display:inline-flex;flex:none}
  /* Bordered at rest, like the two controls beside it. Transparent until
     hover, it was the only thing on that row that did not look pressable
     until you had already found it - and the row reads as a set of three
     buttons, so one of them being a bare word broke the set. */
  .ag-effort-pill{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:8px;
    border:1px solid var(--line);background:var(--card);cursor:pointer;
    font:inherit;font-size:.78rem;font-weight:600;color:var(--ink-2);
    transition:background .18s var(--ease),border-color .18s var(--ease),color .18s var(--ease)}
  /* THE PILL IS AS WIDE AS ITS WIDEST LABEL, ALWAYS.
     The level name changes as you drag, and the names are not the same
     length: "Fast" measured 47.1px and "Balanced" 77.5px, so the pill
     swung 30.4px and every control to its right - the model pill, the
     mic, the send button - slid by exactly that much. Dragging the
     slider made the whole composer row shuffle sideways under the
     cursor, and on a narrow composer it was enough to wrap the row.

     Every label is in the DOM, stacked in one grid cell; the three that
     are not current are visibility:hidden, so they still take part in
     sizing and nothing else. A min-width in px would do the same job
     until the font, the type scale or the wording changed; this cannot
     go stale, because the thing being measured IS the set of labels. */
  .ag-effort-lbl{display:grid;justify-items:center}
  .ag-effort-lbl>*{grid-area:1/1}
  .ag-effort-lbl>i{visibility:hidden;font-style:normal;pointer-events:none}
  .ag-effort-pill:hover{background:var(--paper-2);border-color:var(--line-hover)}
  .ag-effort-pill.open{background:var(--paper-2);border-color:var(--line-hover)}
  /* No indicator dot. The level's NAME is the indicator — a coloured bead
     beside a word that already says the same thing is one signal too many,
     and the reference carries none. The top of the scale still tints the
     label so it is readable at a glance. */
  .ag-effort-pill[data-level="smart"],
  .ag-effort-pill[data-level="max"]{color:var(--accent)}

  .ag-effort-tip{position:absolute;bottom:calc(100% + 7px);left:50%;transform:translateX(-50%) translateY(3px);
    background:var(--ink);color:var(--paper);font-size:.7rem;font-weight:600;
    padding:3px 8px;border-radius:6px;white-space:nowrap;pointer-events:none;
    opacity:0;transition:opacity .14s var(--ease),transform .14s var(--ease);z-index:60}
  .ag-effort-wrap:hover .ag-effort-tip{opacity:1;transform:translateX(-50%) translateY(0)}
  .ag-effort-wrap:has(.ag-effort-pop.open) .ag-effort-tip{opacity:0}

  /* Narrower and shorter than it was. The description line went with it:
     the level name IS the description once you have read it once, and a
     paragraph re-rendering under your thumb is noise while you drag. */
  .ag-effort-pop{position:absolute;bottom:calc(100% + 10px);right:0;width:252px;z-index:70;
    background:var(--card);border:1px solid var(--line);border-radius:15px;padding:13px 15px 14px;
    box-shadow:0 18px 44px -18px rgba(23,23,23,.28),0 2px 8px -5px rgba(23,23,23,.14);
    opacity:0;visibility:hidden;transform:translateY(5px) scale(.975);transform-origin:100% 100%;
    transition:opacity .2s cubic-bezier(.2,.8,.2,1),transform .24s cubic-bezier(.2,.8,.2,1),visibility .24s}
  .ag-effort-pop.open{opacity:1;visibility:visible;transform:none}
  :root[data-theme="dark"] .ag-effort-pop{
    box-shadow:0 22px 48px -18px rgba(0,0,0,.72),0 2px 10px -5px rgba(0,0,0,.5)}

  .ag-effort-head{display:flex;align-items:center;gap:7px;margin-bottom:11px}
  .ag-effort-head .k{font-size:.78rem;font-weight:500;color:var(--mut)}
  .ag-effort-head .v{font-size:.78rem;font-weight:600;color:var(--accent);
    transition:opacity .16s var(--ease)}
  .ag-effort-head .v.swap{opacity:0}
  .ag-effort-head .q{margin-inline-start:auto;width:14px;height:14px;color:var(--mut);flex:none;opacity:.55}

  .ag-effort-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--mut);margin-bottom:6px}

  .ag-effort-row{display:flex;align-items:center;gap:9px}
  .ag-effort-track{position:relative;flex:1 1 auto;height:26px;min-width:0}

  /* THE RAMP IS THE DOTS THEMSELVES.
     Three grids of the same 7px pitch, each a heavier dot than the last,
     faded in over a different stretch of the track. Stacked, the left end is
     a faint speckle and the right is solid — so the control says "more" by
     getting denser, which is the thing you can read without reading.

     There is no lit bar over the top. An overlay that filled to the thumb
     was the first attempt and it destroyed the ramp: at the top of the scale
     it painted full-strength dots across the whole width, so Max and Fast
     looked identical apart from where the handle sat. The gradient is
     static; only the thumb moves. */
  .ag-effort-track .dots,
  .ag-effort-track .dots::before,
  .ag-effort-track .dots::after{
    position:absolute;inset:0;border-radius:8px;content:"";pointer-events:none}
  /* THE GRAIN IS THE SCALE. THE BLUE IS MAX.

     The ramp is a dot grain that densifies toward "Smarter" — that is the
     scale itself, and it is there at every level, because it is what makes
     the positions mean something. What it is NOT is blue: a filled band at
     every level said "full power" four times over and left the top of the
     scale with nothing of its own to say.

     The fill belongs to Max alone, and it arrives rather than appearing —
     wiped in from the left over about a second and a half, with the bright
     head of the run travelling just ahead of it. That is the whole point of
     it: you see the track charge.

     On scale: the pitch was 7px with dots at 26/33/40% of the cell, 3.6 to
     5.6px across and about three and a half rows on a 26px track. At that
     size you do not read a density, you read a printed halftone. 4px pitch
     at ~17% is roughly 1.4px across and six and a half rows, fine enough to
     read as texture.

     Every grain layer sits on the SAME 4px lattice, aligned. Offsetting the
     heavier ones by half a cell was tried and is worse: it puts dots
     BETWEEN the existing dots, so the layers stop being one field at
     several strengths and become overlapping grids — and grids of
     different pitch beat into a plaid you can see at a glance. */
  .ag-effort-track .dots{
    background:var(--paper-2);overflow:hidden;
    background-image:radial-gradient(circle at center,
      color-mix(in srgb,var(--accent) 42%,transparent) 17%, transparent 19%);
    background-size:4px 4px;
    -webkit-mask-image:linear-gradient(90deg,rgba(0,0,0,.13) 0%,rgba(0,0,0,.44) 42%,#000 88%);
    mask-image:linear-gradient(90deg,rgba(0,0,0,.13) 0%,rgba(0,0,0,.44) 42%,#000 88%)}
  .ag-effort-track .dots::before{
    background-image:radial-gradient(circle at center,
      color-mix(in srgb,var(--accent) 66%,transparent) 18%, transparent 20%);
    background-size:4px 4px;
    -webkit-mask-image:linear-gradient(90deg,transparent 14%,rgba(0,0,0,.5) 52%,#000 88%);
    mask-image:linear-gradient(90deg,transparent 14%,rgba(0,0,0,.5) 52%,#000 88%)}
  .ag-effort-track .dots::after{
    background-image:radial-gradient(circle at center,
      var(--accent) 19%, transparent 21%);
    background-size:4px 4px;
    -webkit-mask-image:linear-gradient(90deg,transparent 48%,rgba(0,0,0,.45) 76%,#000 100%);
    mask-image:linear-gradient(90deg,transparent 48%,rgba(0,0,0,.45) 76%,#000 100%)}

  /* ── THE CHARGE ───────────────────────────────────────────────────────
     Max only, and it fills in rather than switching on.

     A MASK, NOT A CLIP. clip-path did the job but its edge is a hard
     vertical line, and a hard line travelling across a track reads as a
     progress bar filling — mechanical, and the one thing this should not
     look like. The mask carries a soft falloff about a third of the track
     wide, so what moves is a gradient shoulder rather than a cut, and the
     band appears to soak across instead of being wiped in.

     The geometry: the mask is 240% of the element, opaque to 42% of its own
     width and transparent by 56%. At mask-position 100% the element shows
     the image's last 41.67%, which is entirely past the fade — fully
     hidden. At 0% it shows the first 41.67%, entirely inside the opaque
     run — fully lit. Sliding between the two therefore reveals left to
     right, the direction the scale reads, with the shoulder sweeping
     across. Both numbers matter: a shorter mask leaves the charged state
     faded at its right edge, a longer one starts already partly visible.

     No transition on the way out. Dragging off Max drops it immediately,
     which is correct: the band means "this one setting", and a band fading
     away over a third of a second while you are already on Smart is the
     band lying about the level. */
  .ag-effort-track .fill{
    position:absolute;inset:0;border-radius:8px;pointer-events:none;z-index:1;
    opacity:0;
    /* Eight stops rather than four. The ramp is the full width of a small
       control, so four left visible steps between them; at this count the
       transition between any two is under the threshold where an edge
       shows. The 180deg layer on top is a single soft highlight along the
       upper edge, which is what stops the band reading as flat ink. */
    background-image:
      linear-gradient(180deg,rgba(255,255,255,.13),rgba(255,255,255,0) 62%),
      linear-gradient(90deg,
        transparent 0%,
        color-mix(in srgb,var(--accent) 4%,transparent) 14%,
        color-mix(in srgb,var(--accent) 11%,transparent) 28%,
        color-mix(in srgb,var(--accent) 21%,transparent) 42%,
        color-mix(in srgb,var(--accent) 34%,transparent) 56%,
        color-mix(in srgb,var(--accent) 47%,transparent) 70%,
        color-mix(in srgb,var(--accent) 58%,transparent) 85%,
        color-mix(in srgb,var(--accent) 66%,transparent) 100%);
    -webkit-mask-image:linear-gradient(90deg,#000 0%,#000 42%,
      rgba(0,0,0,.72) 47%,rgba(0,0,0,.28) 52%,rgba(0,0,0,0) 56%);
    mask-image:linear-gradient(90deg,#000 0%,#000 42%,
      rgba(0,0,0,.72) 47%,rgba(0,0,0,.28) 52%,rgba(0,0,0,0) 56%);
    -webkit-mask-size:240% 100%;mask-size:240% 100%;
    -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
    -webkit-mask-position:100% 0;mask-position:100% 0}
  /* Eased at BOTH ends. A pure ease-out leaves at full speed from a
     standing start, which is the abrupt part of a wipe however soft its
     edge; this leans in, runs, and settles. */
  .ag-effort-track[data-heat="max"] .fill{
    opacity:1;animation:effCharge 1.6s cubic-bezier(.4,0,.2,1) forwards}
  .ag-effort-track.dragging[data-heat="max"] .fill{animation-duration:.85s}
  @keyframes effCharge{
    from{-webkit-mask-position:100% 0;mask-position:100% 0}
    to{-webkit-mask-position:0% 0;mask-position:0% 0}
  }

  /* ── THE FIELD ────────────────────────────────────────────────────────
     Light speckle and a bright head, both of which live ON the charge — a
     white mote over the bare ground is invisible, so this is Max-only too.
     It has to be: the charge is what these are texture and motion FOR. */
  .ag-effort-track .particles{
    position:absolute;inset:0;border-radius:8px;overflow:hidden;
    pointer-events:none;z-index:2;opacity:0;
    /* No transition on opacity, because the field is either on or off and
       a fade buys nothing: it belongs to Max alone, and the charge beneath
       it is already the thing that arrives gradually.

       An earlier comment here claimed a transition on this property "sticks
       because the popover subtree is hidden". That was wrong, and it is
       worth saying so rather than deleting it: the measurement behind it
       was taken in a browser tab that was not painting, where every
       animation clock is throttled to a standstill - a plain <div> created
       on the spot showed currentTime 0 after 900ms and rAF fired once in
       800ms. Nothing about this element is special. Anyone re-measuring
       animation timing on this page should check document.visibilityState
       and rAF cadence first. */
    background-image:radial-gradient(circle at center,
      rgba(255,255,255,.55) 16%,transparent 18%);
    background-size:4px 4px;
    background-position:0 0;
    -webkit-mask-image:linear-gradient(90deg,transparent 22%,rgba(0,0,0,.5) 52%,#000 92%);
    mask-image:linear-gradient(90deg,transparent 22%,rgba(0,0,0,.5) 52%,#000 92%);
    animation:effDrift 4s linear infinite}
  .ag-effort-track[data-heat="max"] .particles{opacity:1}
  .ag-effort-track.dragging[data-heat="max"] .particles{animation-duration:1.7s}
  /* Exactly one cell, so it loops seamlessly and the motion reads as the
     grain travelling rather than the whole panel sliding. */
  @keyframes effDrift{ to{background-position:4px 0} }

  /* Motes flickering on a period that does not divide the drift's, so the
     two never line up into a single pulse. */
  .ag-effort-track .particles::before{
    content:"";position:absolute;inset:0;
    background-image:radial-gradient(circle at center,
      rgba(255,255,255,.8) 15%,transparent 17%);
    background-size:4px 4px;
    -webkit-mask-image:linear-gradient(90deg,transparent 34%,rgba(0,0,0,.4) 62%,#000 100%);
    mask-image:linear-gradient(90deg,transparent 34%,rgba(0,0,0,.4) 62%,#000 100%);
    animation:effTwinkle 2.6s ease-in-out infinite}
  @keyframes effTwinkle{ 0%,100%{opacity:.16} 50%{opacity:.66} }

  /* THE HEAD. A bright leading edge with a tail behind it. Its first pass
     is timed to the charge - same 1.6s, both starting the moment the level
     becomes Max - so the light arrives and the band is left behind it. It
     keeps looping after that, which is what stops a charged track looking
     like a finished progress bar.

     Linear on purpose: an eased sweep reads as a shine passing over glass,
     a constant one reads as something moving under its own power. */
  .ag-effort-track .particles::after{
    content:"";position:absolute;top:-2px;bottom:-2px;width:15%;
    background:linear-gradient(90deg,
      transparent,
      rgba(255,255,255,.28) 38%,
      rgba(255,255,255,.92) 62%,
      rgba(255,255,255,.55) 78%,
      transparent);
    filter:blur(1.5px);
    transform:translateX(-200%);opacity:0}
  .ag-effort-track[data-heat="max"] .particles::after{
    opacity:1;animation:effRun 1.6s linear infinite}
  .ag-effort-track.dragging[data-heat="max"] .particles::after{animation-duration:.95s}
  @keyframes effRun{
    0%{transform:translateX(-200%)}
    100%{transform:translateX(800%)}
  }

  /* At the top the track itself is lit, not just its contents. Max only,
     for the same reason as everything else here. */
  .ag-effort-track .dots{transition:box-shadow .35s var(--ease)}
  .ag-effort-track[data-heat="max"] .dots{
    box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 26%,transparent),
      0 0 15px -2px color-mix(in srgb,var(--accent) 55%,transparent)}

  /* The slow ambient sheen, under the head and over the charge. It is drawn
     in accent, so like everything else blue here it is Max only - at the
     lower levels it was a blue wash crossing a track that is supposed to
     have no blue in it at all. */
  .ag-effort-track .sheen{position:absolute;inset:0;border-radius:8px;overflow:hidden;
    pointer-events:none;z-index:3;opacity:0}
  .ag-effort-track[data-heat="max"] .sheen{opacity:1}
  .ag-effort-track .sheen::after{content:"";position:absolute;top:0;bottom:0;width:38%;
    background:linear-gradient(100deg,transparent,
      color-mix(in srgb,var(--accent) 26%,transparent) 45%,transparent);
    transform:translateX(-140%);
    animation:effSheen 4.4s cubic-bezier(.4,0,.2,1) infinite}
  @keyframes effSheen{
    0%{transform:translateX(-140%)}
    60%{transform:translateX(300%)}
    100%{transform:translateX(300%)}
  }

  /* The visible thumb is a div so it can EASE between steps. A native range
     thumb jumps: the browser paints it at the new value on the same frame,
     so arrow keys and taps both snap. The input is still the control — it
     keeps focus, keyboard and touch — with its own thumb made transparent
     and left full height for the hit area. */
  .ag-effort-thumb{position:absolute;top:50%;left:0;width:24px;height:20px;border-radius:7px;
    /* NO CUSTOM PROPERTY IN HERE — the transform is written from JS.
       Two attempts went through a variable and both failed silently while
       every diagnostic said they should work: calc(var(--n) * var(--len))
       computed to zero, and a single var(--eff-x) latched at whatever value
       it first held. Measured across five steps the thumb reported x=119
       while --eff-x read 0, 59, 119 and 178 in turn — the property updates,
       the transform does not follow it. An unregistered custom property
       inside transform is not a reliable dependency for style recalc here,
       so positionThumb() assigns the whole declaration instead. The
       transition below still does the easing. */
    /* POSITIONED WITH `left`, NOT `transform`.
       Three attempts went through transform and all three failed the same
       way: the computed matrix stayed at one value no matter what was
       written. Measured directly — setting style.transform to
       translate(0px,-50%), even with !important, still rendered at 172px,
       and there was no competing rule and no animation on the element. The
       cause was never identified; `left` is not subject to it, transitions
       identically, and the vertical centring keeps a static transform. */
    transform:translateY(-50%);
    background:#fff;border:1px solid rgba(23,23,23,.16);
    box-shadow:0 1px 3px rgba(23,23,23,.22);
    pointer-events:none;z-index:3;
    transition:left .26s cubic-bezier(.22,1,.28,1),box-shadow .24s var(--ease)}
  /* The halo is the level rather than decoration, so it is a class the two
     top levels carry — driven from JS, which already knows the level, rather
     than from a custom property inside a shadow. */
  .ag-effort-track[data-heat="smart"] .ag-effort-thumb{
    box-shadow:0 1px 3px rgba(23,23,23,.22),0 0 8px color-mix(in srgb,var(--accent) 34%,transparent)}
  .ag-effort-track[data-heat="max"] .ag-effort-thumb{
    box-shadow:0 1px 3px rgba(23,23,23,.22),0 0 13px color-mix(in srgb,var(--accent) 52%,transparent)}
  /* Dragging AT max: the halo gains a hard ring as well as the bloom, so the
     handle reads as charged rather than merely glowing. */
  .ag-effort-track.dragging[data-heat="max"] .ag-effort-thumb{
    box-shadow:0 1px 3px rgba(23,23,23,.22),
      0 0 0 3px color-mix(in srgb,var(--accent) 26%,transparent),
      0 0 18px color-mix(in srgb,var(--accent) 72%,transparent)}
  /* No easing while dragging — a 260ms ease chasing a pointer trails behind
     it, which reads as stutter rather than as smoothness. */
  .ag-effort-track.dragging .ag-effort-thumb{transition:box-shadow .24s var(--ease)}
  .ag-effort-track.dragging .ag-effort-thumb{left:var(--x,0)}
  :root[data-theme="dark"] .ag-effort-thumb{background:#f6f6f8;border-color:rgba(0,0,0,.45);
    box-shadow:0 1px 4px rgba(0,0,0,.6)}
  .ag-effort-range:focus-visible ~ .ag-effort-thumb{
    box-shadow:0 1px 3px rgba(23,23,23,.26),0 0 0 3px color-mix(in srgb,var(--accent) 32%,transparent)}
  .ag-effort-track:active .ag-effort-thumb{transition-duration:.1s}

  .ag-effort-range{position:absolute;inset:0;width:100%;height:100%;margin:0;z-index:4;
    -webkit-appearance:none;appearance:none;background:transparent;cursor:pointer}
  .ag-effort-range:focus{outline:none}
  .ag-effort-range::-webkit-slider-runnable-track{height:26px;background:transparent}
  .ag-effort-range::-moz-range-track{height:26px;background:transparent}
  .ag-effort-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
    width:24px;height:26px;background:transparent;border:0}
  .ag-effort-range::-moz-range-thumb{width:24px;height:26px;background:transparent;border:0}

  /* The same hint the reference carries: enter commits and closes. */
  .ag-effort-enter{flex:none;width:15px;height:15px;color:var(--mut);opacity:.5}

  @media (prefers-reduced-motion:reduce){
    /* Reduced motion means no motion, not no control. This group used to be
       display:none in one rule - pill, popover, thumb and all - so anyone
       with the OS setting on lost the ability to change effort at all
       rather than losing the animation. The control stays; its transitions
       and animations do not. */
    .ag-effort-pop,.ag-effort-tip,.ag-effort-head .v,.ag-effort-thumb,
    .ag-effort-pill,.ag-effort-track .dots{transition:none;animation:none}
    /* These exist only in order to move, so there is nothing left of them
       to keep. The charge goes with them: a band that is supposed to arrive
       is not worth having if it cannot. */
    .ag-effort-track .fill,
    .ag-effort-track .particles,
    .ag-effort-track .sheen{display:none}
  }
  @media (max-width:900px){
    /* Anchor to the COMPOSER, not the pill and not the viewport.
       Pill-anchored, the popover started at -65px on a 375px screen —
       measured, with Faster off the left edge. Viewport-anchored was worse
       in a way that only showed on the new-chat screen, where the composer
       is centred in the hero rather than pinned to the bottom, so it landed
       below the composer on top of the tiles. Making the wrap static hands
       the containing block to .ag-box, which is already position:relative
       and is the thing this should track. */
    .ag-effort-wrap{position:static}
    .ag-effort-pop{left:0;right:0;width:auto;bottom:calc(100% + 10px);transform-origin:50% 100%}
    .ag-effort-tip{display:none}
  }

  .ag-model-wrap{position:relative;display:inline-flex;flex:none}
  .ag-model-pill{display:inline-flex;align-items:center;gap:5px;padding:4px 9px;border-radius:8px;
    border:1px solid var(--line);background:var(--card);color:var(--ink);font-size:.8rem;font-weight:600;
    cursor:pointer;transition:.16s;user-select:none}
  .ag-model-pill:hover{border-color:var(--line-hover);background:var(--paper-2)}
  .ag-model-pill svg{width:11px;height:11px;color:var(--mut);transition:transform .16s ease}
  .ag-model-pill.open svg{transform:rotate(180deg)}

  /* Claude & Replit Popover Menu */
  /* Opens DOWNWARD from the pill, like an ordinary dropdown, and is sized
     to fit its content — no max-height, no inner scrollbar. Opening upward
     was what forced the cap in the first place: the pill sits low in the
     composer, so an upward panel ran out of room above and had to scroll
     inside itself. Downward it has the whole page below it, and the
     section it lives in scrolls if the viewport is genuinely too short. */
  .ag-model-popover{position:absolute;top:calc(100% + 8px);bottom:auto;right:0;left:auto;
    width:min(250px,calc(100vw - 28px));
    max-height:calc(100dvh - 24px);overflow-y:auto;overscroll-behavior:contain;
    background:var(--card);border:1px solid var(--line);border-radius:14px;padding:6px;
    box-shadow:0 16px 40px -10px rgba(11,22,38,.45);
    opacity:0;visibility:hidden;transform:translateY(-6px);
    transition:opacity .16s ease,transform .16s ease,visibility .16s ease;z-index:100}
  .ag-model-popover.open{opacity:1;visibility:visible;transform:none}
  /* Anchored to the pill's TOP edge instead of its bottom. The entrance
     slide flips with it so the panel still moves toward the pill. */
  .ag-model-popover.drop-up{top:auto;bottom:calc(100% + 8px);transform:translateY(6px)}
  .ag-model-popover.drop-up.open{transform:none}
  /* Flipped when there is no room below — the composer sits at the bottom
     of the screen once a conversation starts, and a downward panel there
     is simply cut off by the viewport. Direction is chosen at open time
     (see the pill handler); CSS cannot see the available space. */
  .pop-section-ttl{font-size:.66rem;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.05em;padding:5px 8px 4px}
  .pop-option{display:flex;align-items:flex-start;gap:10px;width:100%;padding:6px 8px;border:1px solid transparent;
    border-radius:10px;background:none;color:var(--ink);cursor:pointer;text-align:left;transition:.14s}
  .pop-option:hover{background:var(--paper-2)}
  .pop-option.on{background:var(--paper-2);border-color:var(--line)}
  .pop-option .pop-ic{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-top:1px;color:var(--accent)}
  .pop-option .pop-ic svg{width:16px;height:16px}
  .ag-pill-icon{display:inline-flex;align-items:center;justify-content:center;color:var(--accent)}
  .ag-pill-icon svg{width:13px;height:13px}
  .pop-option .pop-text b{display:block;font-size:.84rem;font-weight:700;color:var(--ink)}
  .pop-option .pop-text small{display:block;font-size:.72rem;color:var(--mut);line-height:1.25;margin-top:1px}
  .pop-divider{height:1px;background:var(--line);margin:4px 0}
  .pop-switch-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 8px;border-radius:10px;cursor:pointer}
  .pop-switch-row:hover{background:var(--paper-2)}
  .pop-switch-row .pop-text b{display:block;font-size:.83rem;font-weight:700;color:var(--ink)}
  .pop-switch-row .pop-text small{display:block;font-size:.73rem;color:var(--mut);margin-top:2px}

  /* Provider rows. Only providers that can actually RUN are listed: Souqi's
     own models plus whatever the account has saved a key for in Settings.
     Unconfigured providers are not shown as greyed-out teasers — a picker
     full of options that fail on send is worse than a short one — they are
     reached through the "Other providers" link at the bottom instead. */
  .pop-prov{display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border:1px solid transparent;
    border-radius:10px;background:none;color:var(--ink);cursor:pointer;text-align:left;transition:.14s;font:inherit}
  .pop-prov:hover{background:var(--paper-2)}
  .pop-prov.on{background:var(--paper-2);border-color:var(--line)}
  .pop-prov .nm{flex:1;font-size:.83rem;font-weight:600}
  .pop-prov .st{font-size:.68rem;font-weight:600;color:var(--mut);white-space:nowrap}
  .pop-prov .st.ready{color:#2ea87a}
  .pop-prov .st.add{color:var(--accent)}
  .pop-more{text-decoration:none}
  .pop-more .nm{font-weight:500;color:var(--mut)}
  .pop-more .st{display:inline-flex;align-items:center;gap:3px}
  .pop-more .st svg{width:12px;height:12px}
  .pop-more:hover .nm{color:var(--ink)}

  /* iOS / Claude style toggle switch */
  .ag-switch{position:relative;display:inline-block;width:34px;height:20px;flex:none}
  .ag-switch input{opacity:0;width:0;height:0}
  .ag-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:var(--stone);
    transition:.2s;border-radius:999px}
  .ag-slider:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background:#fff;
    transition:.2s;border-radius:50%}
  .ag-switch input:checked + .ag-slider{background:#7c3aed}
  .ag-switch input:checked + .ag-slider:before{transform:translateX(14px)}

  /* Voice Input Microphone button */
  .ag-voice-btn{flex:none;width:30px;height:30px;border:1px solid var(--line);background:transparent;color:var(--ink-2);
    border-radius:6px;display:grid;place-items:center;cursor:pointer;transition:.16s}
  .ag-voice-btn:hover{border-color:var(--line-hover);color:var(--ink)}
  .ag-voice-btn svg{width:15px;height:15px}
  .ag-voice-btn.listening{background:rgba(239,68,68,0.18);border-color:#ef4444;color:#ef4444;animation:micPulse 1.2s infinite alternate}
  @keyframes micPulse{
    0%{box-shadow:0 0 0 0 rgba(239,68,68,0.4);transform:scale(1)}
    100%{box-shadow:0 0 0 6px rgba(239,68,68,0);transform:scale(1.08)}
  }

  /* Rounded square, not the old full circle — matches the attach button
     and every other control's radius (see the sitewide rectangle-button
     pass) rather than being the one round thing in the row. */
  .ag-send{flex:none;width:32px;height:32px;border:0;border-radius:8px;cursor:pointer;background:var(--stone);color:var(--ink-2);display:grid;place-items:center;transition:background .2s,color .2s,transform .16s,box-shadow .2s}
  .ag-send svg{width:15px;height:15px}
  .ag-send.live{background:var(--accent);color:#fff;box-shadow:0 2px 8px -2px rgba(26,166,223,.55)}
  .ag-send.live:hover{background:var(--accent-2);transform:translateY(-1px);box-shadow:0 4px 12px -2px rgba(26,166,223,.6)}
  .ag-send.live:active{transform:scale(.9)}
  .ag-send:disabled{cursor:not-allowed}
  .ag-send.is-cancel{background:rgba(239,68,68,0.15);color:#ef4444}
  .ag-send.is-cancel:hover{background:#ef4444;color:#fff}
  /* Launch: the arrow flies up and out, then the button settles back —
     the send has visibly LEFT rather than just greying out. Driven by a
     class the submit handler adds and self-removes on animationend. */
  .ag-send.sent svg{animation:agLaunch .45s var(--ease) both}
  @keyframes agLaunch{
    0%{transform:none;opacity:1}
    45%{transform:translateY(-16px);opacity:0}
    46%{transform:translateY(10px);opacity:0}
    to{transform:none;opacity:1}
  }
  /* The composer briefly relaxes its focus ring as the message leaves. */
  /* Palette-aware, or sending in Power mode flashes cyan through the
     crimson. --glow-focus also spikes the bloom, so the send reads as a
     flare rather than just a ring. */
  .ag-box.sending{--glow-focus:1.6;border-color:rgb(var(--beam-a));
    box-shadow:
      0 0 0 3px rgba(var(--glow-a),.12),
      0 0 40px -2px rgba(var(--glow-a),.30),
      inset 0 1px 0 rgba(255,255,255,.55)}
  @media (prefers-reduced-motion:reduce){
    .ag-send.sent svg{animation:none}
  }

  /* The button row kept desktop metrics all the way down to 320px.

     It is a nowrap flex row of fixed-width controls needing a constant
     264px, and .ag-box offers 262 at 375, 246 at 360 and 206 at 320. So the
     send button sat 2px past the rounded edge on an iPhone SE and 58px past
     it on anything smaller — the arrow bolted to the outside of the composer.
     Nothing above 640px changes.

     .ag-brow-sp is a zero-width spacer that was still charged a gap on each
     side; an auto margin on the attach button does the same job with one
     fewer child in the row.

     wrap is the floor, not the plan. Below about 340px the row runs out of
     width whatever the metrics are, and a second line is a better answer
     than a control sitting outside the box it belongs to. The row packs to
     the end so that when it does wrap, send stays under the other buttons
     on the right rather than jumping to the left margin. */
  @media (max-width:640px){
    .ag-box{padding-inline:10px}
    .ag-brow{gap:4px;row-gap:6px;justify-content:flex-end}
    .ag-brow-sp{display:none}
    .ag-attach{margin-inline-end:auto}
    .ag-attach,.ag-voice-btn{width:28px;height:28px}
    .ag-send{width:30px;height:30px}
    .ag-effort-pill{padding-inline:7px}
    .ag-model-pill{padding-inline:7px}
  }

  .ag-empty{position:relative;padding:44px 20px 36px;color:var(--ink-2);text-align:center}
  .ag-empty::before{
    content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
    width:620px;height:320px;
    background:radial-gradient(ellipse at center, rgba(192, 132, 252, 0.38) 0%, rgba(56, 189, 248, 0.32) 45%, rgba(167, 139, 250, 0.15) 75%, transparent 100%);
    filter:blur(55px);pointer-events:none;z-index:-1;opacity:0.88;
    animation:replitHeroGlow 8s ease-in-out infinite alternate;
  }
  @keyframes replitHeroGlow{
    0%{transform:translate(-50%,-50%) scale(1) rotate(0deg);opacity:0.75}
    50%{transform:translate(-48%,-52%) scale(1.08) rotate(4deg);opacity:0.95}
    100%{transform:translate(-52%,-48%) scale(0.95) rotate(-3deg);opacity:0.8}
  }
  /* Solid ink, NOT a gradient across the whole line. A full-width text
     gradient was what made this read as broken: the left half went
     near-black, the right went cyan, and the eye had nowhere to land.
     One accent word (.hi below) carries the colour instead — the same
     technique, scoped small enough to read as a decision. */
  /* Gemini's hero proportions: regular weight, moderate size, near-normal
     tracking and open line-height — the airy, unhurried look comes from
     NOT being bold. The visual interest lives in the composer's glow
     below, so the type stays quiet on purpose. */
  .ag-empty h2{font-family:var(--disp);font-weight:400;font-size:clamp(1.85rem,3.6vw,2.9rem);
    letter-spacing:-.01em;line-height:1.16;margin-bottom:14px;color:var(--ink);
    opacity:0;animation:agIn .5s var(--ease) forwards}
  /* Solid accent, no gradient/swash — at 400 weight a decorated word
     fights the lighter type instead of lifting it. */
  .ag-empty h2 .hi{color:var(--accent);white-space:nowrap;transition:color .25s var(--ease)}
  /* Power Agent's crimson theme (see .ag-box.power-mode above) carries up
     into the greeting too, so the whole hero reads as "armed" the moment
     the mode is picked, not just the composer. */
  body.power-mode .ag-empty h2 .hi{color:#e11d48}

  /* New-chat-only extras: type tiles + example prompts, both hidden the
     moment a first turn lands (see isFirstBuild in runTurn()) — the same
     [hidden]-vs-class-display trap as everywhere else in this file, so the
     override is repeated here rather than assumed. */
  .ag-newchat-extras[hidden]{display:none}

  .ag-type-tiles{display:flex;align-items:center;gap:12px;margin:24px 0 20px;justify-content:center}
  .ag-tiles-arrow{flex:none;width:32px;height:32px;border:0;background:var(--paper-2);color:var(--ink-2);border-radius:8px;
    display:grid;place-items:center;cursor:pointer;transition:.2s;font-weight:600}
  .ag-tiles-arrow:hover{background:var(--stone);color:var(--ink);transform:scale(1.05)}
  .ag-tiles-arrow svg{width:18px;height:18px}
  /* Matches Replit's own left arrow at rest (visibly inert, not just
     unclickable) — there's nothing to scroll TO at the start/end of the
     row, or at all when all 5 tiles already fit, so the button says so
     instead of looking clickable and silently doing nothing. */
  .ag-tiles-arrow:disabled{opacity:.35;cursor:default;pointer-events:none}
  .ag-tiles-arrow:disabled:hover{background:var(--paper-2);color:var(--ink-2);transform:none}
  /* justify-content:safe center, not plain center — with overflow-x:auto,
     plain center clips the START of the row off-screen with no way to
     scroll back to it once content is wider than the track (a real
     flexbox footgun). "safe" falls back to flex-start only when it would
     otherwise overflow, so five tiles centre normally and a sixth added
     later doesn't hide "Website" behind the left edge.
     Found live: the comment above was already true, but the actual
     value here was still plain "center" — the mismatch is exactly why
     "Website" rendered cut off at the left edge with 8 tiles.
     max-width caps the visible strip to exactly 5 tiles (5×80 + 4×12
     gap = 448px) regardless of the composer's own width, so the arrows
     page through a clean set of 5 instead of showing a fractional 6th —
     scroll-snap is what makes each arrow click land exactly on a tile
     boundary rather than wherever a fixed pixel scroll happened to stop. */
  /* overflow-x:auto with no explicit overflow-y forces the Y axis to also
     compute as auto (mixing visible with a non-visible value isn't
     allowed) — so this box clips vertically too, and the hover lift/glow
     on a tile were getting sliced off at the top edge. Top padding gives
     them room inside the box instead of trying to fight the clip with
     overflow-y:visible (which would just get forced back to auto). */
  .ag-tiles-track{flex:0 1 auto;max-width:448px;display:flex;justify-content:safe center;gap:12px;
    overflow-x:auto;scrollbar-width:none;scroll-behavior:smooth;scroll-snap-type:x mandatory;padding:14px 4px 4px}
  .ag-tiles-track::-webkit-scrollbar{display:none}
  .ag-tile{flex:none;display:flex;flex-direction:column;align-items:center;gap:10px;border:0;background:none;
    color:var(--ink-2);font-size:.82rem;font-weight:500;cursor:pointer;width:80px;transition:.15s;scroll-snap-align:start}
  .ag-tile .ic{width:56px;height:56px;border-radius:12px;background:var(--paper-2);border:1.5px solid var(--line);
    display:grid;place-items:center;color:var(--ink-2);transition:.2s}
  .ag-tile .ic svg{width:24px;height:24px}
  .ag-tile:hover .ic{border-color:var(--line-hover);color:var(--ink);transform:translateY(-2px)}
  .ag-tile.on .ic{background:var(--accent);border-color:var(--accent);color:#fff;
    box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 16%,transparent),0 4px 14px -4px color-mix(in srgb,var(--accent) 65%,transparent)}
  .ag-tile.on{color:var(--ink);font-weight:600}

  .ag-examples{text-align:center;margin-top:20px}
  .ag-examples .lbl{display:inline-flex;align-items:center;gap:7px;font-size:.82rem;color:var(--ink-2);margin-bottom:12px;font-weight:500}
  .ag-examples .lbl button{border:0;background:none;color:var(--ink-2);cursor:pointer;width:20px;height:20px;
    border-radius:6px;display:grid;place-items:center;transition:.15s}
  .ag-examples .lbl button:hover{background:var(--paper-2);color:var(--ink);transform:rotate(60deg)}
  .ag-examples .lbl button svg{width:14px;height:14px}
  .ag-ex-row{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
  .ag-ex-chip{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:8px;
    padding:8px 12px;font-size:.82rem;font-weight:400;cursor:pointer;transition:.15s}
  .ag-ex-chip:hover{border-color:#b8cdd7;background:var(--paper-2)}

  /* Recent projects — matched to Replit's own cards: a large neutral
     thumbnail area holding one dark rounded tile (NOT a full-bleed
     coloured panel with a giant letter, which is what this was), a
     "View All" pill button rather than a bare text link, and body type at
     regular weight in the UI font. */
  .ag-recent {
    margin: 56px auto 40px;
    max-width: 860px;
    width: 100%;
    padding-top: 0;
    border-top: 0;
    box-sizing: border-box;
  }
  .ag-recent-head { display: flex; align-items: center; margin-bottom: 18px; width: 100%; }
  .ag-recent-head h3 { font-family: var(--body); font-size: 1.16rem; font-weight: 400; letter-spacing: -.01em; color: var(--ink); }
  .ag-recent-head button {
    margin-inline-start: auto; display: flex; align-items: center; gap: 7px; border: 0;
    background: var(--paper-2); color: var(--ink); font-size: 0.92rem; font-weight: 400; cursor: pointer;
    padding: 9px 15px; border-radius: 9px; transition: 0.15s;
  }
  .ag-recent-head button:hover { background: var(--stone); }
  .ag-recent-head button svg { width: 16px; height: 16px; }

  /* Recent Projects Grid — Replit Style */
  .ag-recent-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
    justify-content: stretch;
    width: 100%;
    box-sizing: border-box;
  }
  .ag-recent-empty {
    grid-column: 1 / -1;
    padding: 24px 16px;
    color: var(--mut);
    font-size: 0.86rem;
    text-align: center;
    line-height: 1.5;
    border: 1px dashed var(--line);
    border-radius: 14px;
  }
  /* The card itself is styles/components/project-card.css now, the same file
     projects.html and deployments.html use. What stood here was a third copy
     that had drifted — a flat 135px band instead of a 16/9 frame, a 56px tile
     instead of 54, no inner highlight, no badge, and none of the sheen or
     hover glow — so the same app looked like a different project depending on
     which page you opened it from. The grid above stays: how many columns the
     builder's landing screen wants is a page decision, not a card one. */

  /* DESKTOP VIEW: 3 cards when preview is closed, 2 cards when preview is open */
  @media (min-width: 901px) {
    .has-preview .ag-recent,
    .ag-split.has-preview .ag-recent {
      max-width: 100% !important;
      margin: 36px 0 24px !important;
    }
    .has-preview .ag-recent-grid,
    .ag-split.has-preview .ag-recent-grid {
      grid-template-columns: repeat(2, 1fr) !important;
      gap: 10px !important;
    }
    /* Only the column count is a page decision. The card sizes itself from
       its own stylesheet, including beside an open preview. */
  }

  /* MOBILE VIEW: ONLY 2 FIT HORIZONTALLY */
  @media (max-width: 900px) {
    .ag-recent {
      max-width: 100% !important;
      margin: 32px 0 20px !important;
    }
    .ag-recent-grid {
      grid-template-columns: repeat(2, 1fr) !important;
      gap: 10px !important;
    }
    /* The card's own phone sizing lives with the card now; project-card.css
       carries a max-width:640px block of its own. What was here scaled a
       thumb and a body that this page no longer builds. */
  }

  /* No own height/sticky — the app shell (html/body/.ag-main, below) is
     fixed to the viewport and this just fills its grid cell. It used to be
     `position:sticky; height:100svh`, which sat BELOW a 64px header and so
     made the document 64px taller than the screen: the whole page scrolled
     and the header slid away, which reads as a bug rather than a layout. */
  .ag-preview{position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--paper-2)}
  @media (min-width:901px){
    /* Collapsed until something's been built, so the new-chat screen is
       .ag alone rather than a wide empty pane beside a narrow chat — then
       expands in step with .ag narrowing (both flex-basis transitions
       fire together). Scoped to the SPLIT, which is a mouse shape: where
       the panes swap instead of sitting side by side, .tab-hidden owns
       showing and hiding this panel and the block below takes the
       collapse back off. */
    .ag-preview{flex:0 0 0;width:0;overflow:hidden;opacity:0;
      transition:flex-basis .4s var(--ease),opacity .25s var(--ease)}
    .ag-split.has-preview .ag-preview{flex:1 1 auto;width:auto;opacity:1;
      transition:flex-basis .4s var(--ease),opacity .3s var(--ease) .1s}
  }
  /* THE PANE SWAP IS A TOUCH SHAPE, NOT A NARROW ONE.

     Same reasoning as the settings overlay further down, and the same
     device: an iPad in landscape reports 1180 or 1366 CSS pixels, wider
     than most laptops. Every rule that decided this by width alone
     handed a tablet the desktop split — so "Preview" opened a 340px
     sidecar beside a chat column that stayed put, on a screen with no
     mouse to justify either. The pointer is what separates them.

     Two things have to move up here, because both were written for the
     phone and stop at 900px:

       - .tab-hidden has to actually hide. It does its hiding inside
         max-width:900, so above that the class went on and nothing
         happened — which is why the chat stayed on screen next to the
         preview.

       - .ag-preview has to come out of the collapse above. That rule
         assumes the split and pins the pane to width:0 unless
         .has-preview is set, so even with the chat gone the preview
         would have opened to nothing. Here the preview IS the screen
         rather than a column beside the chat, so being the pane that is
         not hidden is the whole condition.

     The toggle button itself is a child of .ag-main, a sibling of
     .ag-split — so hiding .ag never takes "Back to chat" down with it
     and there is always a way out. */
  @media (hover:none) and (pointer:coarse){
    .ag.tab-hidden,.ag-preview.tab-hidden{display:none !important}
    .ag.tab-hidden .ag-composer{display:none !important}
    .ag-preview:not(.tab-hidden){flex:1 1 auto;width:auto;opacity:1;overflow:visible}
  }
  .ag-topbar{display:flex;align-items:center;width:100%;border-bottom:1px solid var(--line);flex:none}
  /* A direct child of .ag-preview (position:relative), not of
     .ag-pv-stage — .ag-pv-stage clips (overflow:hidden), which left no
     room to sit any higher than its own padding allowed. .ag-preview
     itself doesn't clip, so this can sit right at the top of the panel,
     centered above whichever device mockup .ag-pv-stage is showing. */
  /* ---- Publish ----
     The class has been styled since the beginning (see the phone rule
     further down, which reflows "label + device toggle + Publish") but no
     element ever carried it, so the button did not exist. It sits opposite
     the device toggle rather than inside it: those switch a preview size,
     this ships the app, and they should not look like the same kind of
     control. */
  .ag-pv-publish{position:absolute;top:6px;right:150px;z-index:46;
    display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 14px;
    border:0;border-radius:9px;background:var(--accent);color:#fff;
    font-family:var(--body);font-size:.82rem;font-weight:600;cursor:pointer;
    box-shadow:0 4px 14px -6px rgba(11,22,38,.4);transition:background .15s,opacity .15s}
  .ag-pv-publish:hover{background:var(--accent-2)}
  .ag-pv-publish:disabled{opacity:.55;cursor:default}
  .ag-pv-publish svg{width:14px;height:14px;flex:none}
  /* Once it is live the button becomes the way to the app. */
  .ag-pv-publish.live{background:#22a565}
  .ag-pv-publish.live:hover{background:#1b8b53}

  .ag-pv-devices{position:absolute;top:6px;left:50%;transform:translateX(-50%);z-index:45;
    display:flex;gap:2px;align-items:center;background:var(--card);border:1px solid var(--line);
    border-radius:9px;padding:3px;box-shadow:0 4px 14px -6px rgba(11,22,38,.28)}
  .ag-pv-devices button{border:0;background:none;width:28px;height:26px;border-radius:6px;display:grid;place-items:center;
    color:var(--ink-2);cursor:pointer;transition:.14s}
  .ag-pv-devices button svg{width:15px;height:15px}
  .ag-pv-devices button:hover{background:var(--tint-2);color:var(--accent)}
  .ag-pv-devices button.on{background:var(--tint-2);color:var(--accent)}
  .ag-pv-devices:disabled,.ag-pv-devices[aria-disabled="true"] button{opacity:.5;cursor:default;pointer-events:none}
  /* Extra padding-top reserves real room for the floating device toggle
     (an absolutely-positioned sibling in .ag-preview, so it doesn't
     naturally push this box's centered content down on its own) — without
     it, the mockup centers through the toggle's own space on a shorter
     viewport and the two visibly collide. */
  /* perspective is what makes the desktop frame's lid-open rotateX read
     as a hinge instead of a vertical squash — see @keyframes macEnter. */
  /* container-type:size is what lets every mockup below size itself
     against THIS BOX in both axes — 100cqw/100cqh are the stage's content
     width and height, padding already subtracted.

     It is needed because a device has one dimension and a ratio, and the
     limit that binds is usually on the OTHER axis: the iPad is sized by
     height, but what clipped it was the pane being too narrow, and a
     percentage inside a height property can only ever see the parent's
     height. Container units cross the axes, so "as tall as this box
     allows, but no taller than its width can carry" is one min(). The
     stage's own size never came from its contents (it is flex:1 in a
     column with a definite height, or a min-height below 901px), so size
     containment changes nothing about how big it is. */
  .ag-pv-stage{position:relative;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;
    padding:clamp(12px,2.5vw,24px);padding-top:64px;overflow:hidden;background:var(--paper);
    container-type:size;
    perspective:1600px}
  .ag-pv-frame{
    position:relative;
    transition:all .3s cubic-bezier(0.16,1,0.3,1);
    flex:none;box-sizing:border-box;
    overflow:hidden;
  }
  /* iframe fills the screen wrapper — base, overridden per-device below.
     Scrollbar hidden on the FRAME itself (not injected into its content),
     which is what makes this work even for the WebContainer preview —
     that's a genuinely cross-origin document (its own dev-server URL), so
     nothing this page injects into its content can ever reach it; hiding
     chrome on the <iframe> element is a parent-page style, unaffected by
     the origin of what's inside. Scrolling itself still works via wheel/
     touch — only the scrollbar chrome is hidden, same trick as the srcdoc
     fallback and the WebContainer best-effort injection use for content
     this page CAN reach. */
  /* THE PREVIEW IS A DEVICE, NOT A LEFTOVER STRIP.
     ------------------------------------------------------------------
     This used to be width:100%;height:100% — the app was handed whatever
     space the pane happened to have. On a 767px browser window the chat
     column takes 420px (see .ag-split.has-preview rules), so the "phone"
     was about 223px wide: narrower than any phone ever made. Even the
     roomy case was wrong — 9/19.5 of a 580px-tall frame is ~250px, where
     a real iPhone is 390.

     That is not a cosmetic difference. Tailwind's sm: breakpoint is
     640px and the app's own media queries key off the viewport, so at
     250px the preview showed a layout no actual device would render. An
     app could be perfectly responsive and still look broken here, which
     is exactly the wrong signal to give someone about their own build.

     So the iframe is laid out at TRUE logical device pixels and scaled
     down visually with a transform. transform does not change layout, so
     the document inside still measures 390px (or 834, or 1280) and picks
     the breakpoints a real device would. --pv-w/--pv-h/--pv-scale are
     written by syncDeviceViewport() from the screen's measured size. */
  .ip-screen iframe, .ag-pv-frame iframe{
    position:absolute;top:0;left:0;
    width:var(--pv-w, 100%);
    height:var(--pv-h, 100%);
    transform:scale(var(--pv-scale, 1));
    transform-origin:top left;
    border:0;display:block;
    scrollbar-width:none;-ms-overflow-style:none;
  }
  .ip-screen iframe::-webkit-scrollbar, .ag-pv-frame iframe::-webkit-scrollbar{display:none;width:0;height:0}

  /* ---------- iPhone Screen Wrapper (base) ---------- */
  .ip-screen{
    position:relative;width:100%;height:100%;overflow:hidden;
  }

  /* ---------- iPhone Side Buttons ---------- */
  .ib{display:none;position:absolute;pointer-events:none;z-index:40}
  .ag-pv-frame.mobile .ib{display:block}
  .ib-action{
    left:-3px;top:17%;width:3px;height:28px;
    background:linear-gradient(180deg,#5a5a5e,#3a3a3c 40%,#4a4a4e 60%,#3a3a3c);
    border-radius:2px 0 0 2px;
    box-shadow:-1px 0 2px rgba(0,0,0,0.4);
  }
  .ib-volup{
    left:-3px;top:26%;width:3px;height:46px;
    background:linear-gradient(180deg,#5a5a5e,#3a3a3c 30%,#4a4a4e 70%,#3a3a3c);
    border-radius:2px 0 0 2px;
    box-shadow:-1px 0 2px rgba(0,0,0,0.4);
  }
  .ib-voldn{
    left:-3px;top:36%;width:3px;height:46px;
    background:linear-gradient(180deg,#5a5a5e,#3a3a3c 30%,#4a4a4e 70%,#3a3a3c);
    border-radius:2px 0 0 2px;
    box-shadow:-1px 0 2px rgba(0,0,0,0.4);
  }
  .ib-power{
    right:-3px;top:28%;width:3px;height:64px;
    background:linear-gradient(180deg,#5a5a5e,#3a3a3c 30%,#4a4a4e 70%,#3a3a3c);
    border-radius:0 2px 2px 0;
    box-shadow:1px 0 2px rgba(0,0,0,0.4);
  }

  /* ═══════════════════════════════════════════════════════════════════
     1. DESKTOP — Space Black MacBook Pro
     Matches the hero flank mockup on home.html (.macbook-lid /
     .macbook-screen / .macbook-base there), so the marketing page and
     the product preview show the same machine. This used to be a silver
     Studio Display on a stem-and-foot stand; the frame IS the lid now,
     and .mac-stand below is repurposed into the base — see the comment
     on that rule for why the markup kept its old class names.
     ═══════════════════════════════════════════════════════════════════ */
  .ag-pv-frame.desktop {
    /* Width drives the lid and 16/9 derives the height, so BOTH limits
       have to be written as widths. max-height could not be one: with a
       definite width and an auto height, clamping the height does not
       send the browser back to recompute the width, so a short pane
       squashed the lid out of 16/9 instead of standing a smaller machine
       in it. The 44px is the base — .mac-stand hangs 21px below the lid
       and is 15px tall — plus a little for its cast shadow. */
    width: min(800px, calc(100cqw - 16px), calc((100cqh - 44px) * 16 / 9));
    height: auto; max-height: none;
    aspect-ratio: 16 / 9;
    overflow: visible;
    /* A real MacBook reads as a BLACK bezel with only a thin silver
       chassis edge showing around it — not a wide silver picture-frame,
       which is what a thick light border looks like (and it looked like
       cheap plastic). So: the border is the black bezel, and the silver
       is a 2px box-shadow ring just outside it, with a brighter hairline
       on top of that for the machined-edge highlight. */
    background: #0a0a0c;
    border: 10px solid #0b0b0d;
    padding: 0;
    /* Tight radius — MacBook display corners are subtle, not pill-like.
       Square at the bottom so the lid meets the base flush. */
    border-radius: 11px 11px 3px 3px;
    position: relative;
    transform-origin: bottom center;
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease;
    box-shadow:
      0 0 0 2px #b4b4bd,                    /* silver chassis edge      */
      0 0 0 2.5px rgba(255,255,255,0.55),   /* machined highlight       */
      0 30px 60px -18px rgba(0,0,0,0.55),   /* cast shadow              */
      0 6px 16px -6px rgba(0,0,0,0.35);
  }
  .ag-pv-frame.desktop::before {
    /* FaceTime camera, centred in the top bezel */
    content: "";
    position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
    width: 5px; height: 5px; background: #2a2a30; border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.35);
    z-index: 40; pointer-events: none;
  }
  .ag-pv-frame.desktop::after { display: none; }

  .ag-pv-frame.desktop .ip-screen {
    /* MUST match the bezel's INNER radius, which is the frame's
       border-radius minus its border-width (11px - 10px = 1px). When the
       screen was rounded more than the bezel's inner curve, the chassis
       showed through as a bright wedge in each corner — that was the
       ugly "edges" artifact, not the bezel itself. */
    width: 100%; height: 100%; border-radius: 2px;
    background: #020203; position: relative; overflow: hidden;
    box-shadow:
      inset 0 0 50px rgba(0,0,0,0.75),
      inset 0 0 1px rgba(255,255,255,0.06);
  }
  .ag-pv-frame.desktop .ip-screen::after {
    content: "";
    position: absolute; inset: 0; z-index: 25; pointer-events: none;
    background:
      linear-gradient(175deg,
        rgba(255,255,255,0.04) 0%, transparent 20%,
        rgba(255,255,255,0.02) 50%, transparent 80%);
  }
  .ag-pv-frame.desktop iframe {
    border-radius: 0; position: absolute; top: 0; left: 0;
    width: var(--pv-w, 100%); height: var(--pv-h, 100%);
    border: 0; display: block;
  }
  .ag-pv-frame.desktop > .ib { display: none; }
  .ag-pv-frame.desktop .macbook-body,
  .ag-pv-frame.desktop .macbook-ports { display: none; }

  /* ═══ MacBook base (the lower half + hinge) ═══
     Still called .mac-stand in the markup because it used to be a Studio
     Display stem-and-foot stand. Rather than churn the HTML and the JS
     that walks it, the same two elements are restyled: .stem is now the
     hinge notch, .foot is unused. Kept desktop-only, same as before. */
  .mac-stand {
    display: none;
    position: absolute;
    /* An absolutely-positioned child is offset from its container's
       PADDING box, which excludes the lid's border — so the offset has to
       cover the border AND this element's own height for the base to sit
       flush under the lid rather than overlapping it:
       10px border + 11px .foot height = 21px. (Get this wrong and the
       base sits INSIDE the lid — at -16px against a 13px border only 3px
       of it showed.) left/right are -10px for the same reason: that
       lands them exactly on the lid's outer edge. */
    bottom: -21px; left: -10px; right: -10px;
    z-index: 32; pointer-events: none;
  }
  .ag-pv-frame.desktop .mac-stand { display: block; }
  .mac-stand .stem {
    /* Hinge notch — the shallow cut-out centred on the front edge.
       Darker than the base so it reads as a recess, not a raised tab. */
    position: absolute; top: 0; left: 42%; width: 16%; height: 5px;
    background: linear-gradient(180deg, #9a9aa2 0%, #b4b4bc 100%);
    border-radius: 0 0 6px 6px;
    z-index: 1;
  }
  .mac-stand .foot {
    /* The base slab itself — same silver family as the lid, shaded
       downward so it reads as sitting under the hinge. */
    width: 100%; height: 15px;
    background: linear-gradient(180deg, #d8d8de 0%, #c4c4cc 45%, #b0b0b8 100%);
    border-radius: 0 0 17px 17px;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.5),
      inset 0 -3px 6px rgba(0,0,0,0.18),
      0 14px 26px -10px rgba(0,0,0,0.45);
  }

  /* Entry animation — the lid opens. transform-origin is already
     bottom center on the frame, and .ag-pv-stage supplies the
     perspective, so rotateX reads as a hinge rather than a squash. */
  .ag-pv-frame.desktop.entering {
    animation: macEnter 0.85s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  }
  @keyframes macEnter {
    0%   { transform: rotateX(-78deg) translateY(6px); opacity: 0.15; }
    60%  { opacity: 1; }
    100% { transform: rotateX(0deg) translateY(0); opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    .ag-pv-frame.desktop.entering { animation: none; }
  }

  /* ═══════════════════════════════════════════════════════════════════
     2. TABLET — Photorealistic iPad Pro 12.9" (Space Gray)
     ═══════════════════════════════════════════════════════════════════ */
  .ag-pv-frame.tablet {
    /* The third term is a width limit this frame never had. It is sized
       by height with 3/4 deriving the width, and nothing checked that the
       derived width fit: at an iPad-sized window with the sidebar open
       the preview column is ~417px and this came out 435px, so the stage
       (overflow:hidden) cut the mockup down both edges — no right bezel,
       no left one, just the app running off the pane. Expressed as the
       height the stage's width can carry, it makes the iPad smaller
       instead of clipped. */
    height: min(580px, 100cqh, calc((100cqw - 16px) * 4 / 3));
    max-height: none;
    width: auto; aspect-ratio: 3 / 4;
    /* Space Gray aluminum — smooth metallic gradient */
    background: linear-gradient(
      155deg,
      #b4b6be 0%, #c0c2ca 5%, #a0a2aa 12%, #aaaaaa 22%,
      #8c8e96 35%, #9a9ca4 48%, #84868e 58%, #92949c 70%,
      #7e8088 80%, #8c8e96 90%, #a0a2aa 100%
    );
    border: none;
    padding: 9px;
    border-radius: 28px;
    position: relative;
    transform-origin: center center;
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease;
    box-shadow:
      0 30px 70px -8px rgba(0,0,0,0.5),
      0 10px 20px -5px rgba(0,0,0,0.35),
      0 0 0 0.5px rgba(255,255,255,0.2),
      inset 0 0.5px 0 rgba(255,255,255,0.1);
  }
  .ag-pv-frame.tablet .ip-screen {
    border-radius: 20px;
    background: #050507;
    width: 100%; height: 100%;
    position: relative; overflow: hidden;
    box-shadow:
      inset 0 0 40px rgba(0,0,0,0.7),
      inset 0 0 1px rgba(255,255,255,0.05);
  }
  .ag-pv-frame.tablet::before {
    /* Front-facing camera — in the top bezel */
    content: "";
    position: absolute; top: 4px; left: 50%; transform: translateX(-50%);
    width: 4px; height: 4px; background: #0a0c0e; border-radius: 50%;
    z-index: 40; pointer-events: none;
    box-shadow:
      0 0 0 1px rgba(255,255,255,0.1),
      inset 0 0 1px rgba(60,60,120,0.5);
  }
  .ag-pv-frame.tablet .ip-screen::before { display: none; }
  /* Home bar */
  .ag-pv-frame.tablet .ip-screen::after {
    content: "";
    position: absolute; bottom: 6px; left: 50%; transform: translateX(-50%);
    width: 130px; height: 4px;
    background: rgba(255,255,255,0.25); border-radius: 4px;
    z-index: 30; pointer-events: none;
  }
  .ag-pv-frame.tablet iframe { border-radius: 20px; }
  .ag-pv-frame.tablet > .ib { display: none; }

  /* iPad entry animation */
  .ag-pv-frame.tablet.entering {
    animation: tabletEnter 0.85s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  }
  @keyframes tabletEnter {
    0% {
      transform: perspective(800px) rotateY(-12deg) rotateX(8deg) scale(0.82);
      opacity: 0.2; filter: brightness(0.3);
    }
    100% {
      transform: perspective(800px) rotateY(0deg) rotateX(0deg) scale(1);
      opacity: 1; filter: brightness(1);
    }
  }

  /* 3. MOBILE — iPhone 15 Pro Natural Titanium */
  .ag-pv-frame.mobile {
    overflow: visible; /* side buttons extend outside */
    /* Same pair of limits as the iPad above. The phone is narrow enough
       that it rarely binds, but "rarely" was three pixels: in a 274px
       preview column the 268px body plus its side buttons filled the
       stage edge to edge. 8px, not the 16 the other two take: this frame
       is overflow:visible and what hangs outside it is 3px of buttons,
       not a 70px shadow. */
    height: min(580px, 100cqh, calc((100cqw - 8px) * 19.5 / 9));
    max-height: none;
    aspect-ratio: 9 / 19.5;
    width: auto;
    border: none;
    padding: 10px;
    border-radius: 44px;
    background: linear-gradient(
      145deg,
      #3a3a3c 0%, #58585c 8%, #48484a 22%,
      #3a3a3c 38%, #52525a 52%, #48484a 66%,
      #3a3a3c 82%, #58585c 94%, #3a3a3c 100%
    );
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.15),
      inset 0 -1px 0 rgba(0,0,0,0.25),
      0 0 0 1.5px #1a1a1c,
      0 0 0 2.5px #2c2c2e,
      0 0 0 3.5px #1a1a1c,
      0 20px 50px -8px rgba(0,0,0,0.5),
      0 10px 20px -5px rgba(0,0,0,0.3);
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    flex: none;
    transform-origin: center center;
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s ease;
  }
  .ag-pv-frame.mobile .ip-screen {
    border-radius: 34px;
    background: #000;
    box-shadow: inset 0 0 0 0.5px rgba(255,255,255,0.08);
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  /* Dynamic Island */
  .ag-pv-frame.mobile .ip-screen::before {
    content:""; position:absolute; top:10px; left:50%; transform:translateX(-50%);
    width:100px; height:28px; background:#000; border-radius:20px;
    box-shadow:0 0 0 2px rgba(0,0,0,0.85);
    z-index:50; pointer-events:none;
  }
  /* Home Indicator */
  .ag-pv-frame.mobile .ip-screen::after {
    content:""; position:absolute; bottom:7px; left:50%; transform:translateX(-50%);
    width:120px; height:4px; background:rgba(255,255,255,0.3); border-radius:4px;
    z-index:30; pointer-events:none;
  }
  /* The boot screen and the empty state are chrome: they belong to the
     frame and fill it. The iframe is the DEVICE and must not — it is laid
     out at --pv-w/--pv-h and scaled. Splitting them matters because this
     rule is more specific than the base one and was quietly putting the
     app back to width:100%, i.e. back to the leftover-strip viewport. */
  .ag-pv-frame.mobile .ag-pv-boot, .ag-pv-frame.mobile .ag-pv-empty {
    border-radius: 34px;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }
  .ag-pv-frame.mobile iframe {
    border-radius: 34px;
    position: absolute;
    top: 0; left: 0;
    width: var(--pv-w, 100%);
    height: var(--pv-h, 100%);
  }
  .ag-pv-frame.mobile::before,.ag-pv-frame.mobile::after{display:none}

  /* ═══════════════════════════════════════════════════════════════════
     Mac Boot Screen — white bg, black Apple logo, progress bar on build
     ═══════════════════════════════════════════════════════════════════ */
  .ag-pv-boot {
    position: absolute; inset: 0;
    background: #ffffff;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0;
    z-index: 35;
    transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
    pointer-events: none;
    border-radius: inherit;
  }
  .ag-pv-boot.hidden {
    opacity: 0; visibility: hidden;
    transition: opacity 0.35s ease, visibility 0s 0.35s;
  }
  .ag-pv-boot .apple-logo {
    width: clamp(52px, 10vw, 72px);
    height: clamp(52px, 10vw, 72px);
    color: #1d1d1f;
    display: block; flex: none;
    /* No glow on white bg — clean, sharp Apple silhouette */
    filter: none;
  }
  .ag-pv-boot .boot-label {
    color: rgba(0,0,0,0.45);
    font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Inter', system-ui, sans-serif;
    font-size: clamp(12px, 2vw, 15px);
    font-weight: 400;
    letter-spacing: 0.01em;
    text-align: center;
    line-height: 1.3;
    margin-top: 20px;
  }
  /* Progress bar — hidden until build starts (see JS) */
  .ios-progress-bar {
    width: clamp(130px, 22vw, 180px);
    height: 3px;
    background: rgba(0, 0, 0, 0.1);
    border-radius: 3px;
    overflow: hidden;
    position: relative;
    flex: none;
    margin-top: 24px;
    opacity: 0;
    transition: opacity 0.3s ease;
  }
  .ios-progress-bar.on {
    opacity: 1;
  }
  .ios-progress-fill {
    height: 100%;
    background: #1d1d1f;
    border-radius: 3px;
    width: 0%;
    animation-duration: 3s;
    animation-timing-function: cubic-bezier(0.15, 0.85, 0.35, 1);
    animation-fill-mode: forwards;
    animation-play-state: paused;
    animation-name: iosRealBootFill;
  }
  .ios-progress-fill.running {
    animation-play-state: running;
  }
  @keyframes iosRealBootFill {
    0%   { width: 0%; }
    12%  { width: 8%; }
    28%  { width: 22%; }
    45%  { width: 38%; }
    62%  { width: 58%; }
    78%  { width: 76%; }
    90%  { width: 91%; }
    100% { width: 100%; }
  }

  /* Device-specific boot overrides */
  .ag-pv-frame.desktop .ag-pv-boot {
    border-radius: 0;
  }
  .ag-pv-frame.desktop .ag-pv-boot .apple-logo {
    width: 60px; height: 60px;
  }
  .ag-pv-frame.desktop .ios-progress-bar {
    width: 160px; height: 2.5px;
  }

  .ag-pv-frame.tablet .ag-pv-boot {
    border-radius: 20px;
  }
  .ag-pv-frame.tablet .ag-pv-boot .apple-logo {
    width: 56px; height: 56px;
  }
  .ag-pv-frame.tablet .ios-progress-bar {
    width: 140px; height: 2.5px;
  }

  .ag-pv-frame.mobile .ag-pv-boot {
    border-radius: 34px;
  }
  .ag-pv-frame.mobile .ag-pv-boot .apple-logo {
    width: 50px; height: 50px;
  }
  .ag-pv-frame.mobile .ios-progress-bar {
    width: 126px; height: 2.5px;
  }

  /* ══════ Dark mode — boot screen & display overrides ══════ */
  :root[data-theme="dark"] .ag-pv-boot {
    background: #0e1117;
  }
  :root[data-theme="dark"] .ag-pv-boot .apple-logo {
    color: #e6edf3;
  }
  :root[data-theme="dark"] .ag-pv-boot .boot-label {
    color: rgba(255,255,255,0.45);
  }
  :root[data-theme="dark"] .ios-progress-bar {
    background: rgba(255,255,255,0.12);
  }
  :root[data-theme="dark"] .ios-progress-fill {
    background: #e6edf3;
  }
  :root[data-theme="dark"] .ag-pv-empty {
    background: #0e1117; color: rgba(255,255,255,0.5);
  }
  :root[data-theme="dark"] .ag-pv-stage {
    background: var(--paper);
  }
  :root[data-theme="dark"] .ag-pv-frame.desktop .ip-screen {
    background: #020203;
  }
  :root[data-theme="dark"] .ag-pv-frame.tablet .ip-screen {
    background: #020203;
  }
  :root[data-theme="dark"] .ag-pv-frame.mobile .ip-screen {
    background: #020203;
  }

  /* iPhone entry animation — rotates in like picking up the phone */
  .ag-pv-frame.mobile.entering {
    animation: phoneEnter 0.75s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  }
  @keyframes phoneEnter {
    0% {
      transform: perspective(600px) rotateY(18deg) rotateX(10deg) scale(0.75) translateY(30px);
      opacity: 0.15; filter: brightness(0.2);
    }
    100% {
      transform: perspective(600px) rotateY(0deg) rotateX(0deg) scale(1) translateY(0);
      opacity: 1; filter: brightness(1);
    }
  }

  /* Fills the screen; the TEXT is what gets the narrow column, not the
     element. This used to do it with margin:auto + max-width:28ch, which
     shrinks the element itself — and since it also paints a background,
     that drew a 265px band of card colour down the middle of a 471px
     laptop screen instead of filling it. Only the phone escaped, because
     a separate rule forces width/height:100% there. The padding-inline
     below reproduces the 28ch measure without resizing the box. */
  .ag-pv-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;
    color:var(--mut);font-size:.94rem;line-height:1.5;
    padding-block:24px;padding-inline:max(24px, calc((100% - 28ch) / 2));
    z-index:5;background:#fff;pointer-events:none}
  /* Radius only. This rule used to repeat `background:#fff`, which is already
     set above — and repeating it here was not harmless: it ties with the dark
     override on specificity and wins on source order, so in dark mode the
     empty state rendered white text on a white card inside a black phone. */
  .ag-pv-frame.mobile .ag-pv-empty{border-radius:42px}

  /* Preview paywall — the built app is real and running the moment the
     first build finishes, but its PIXELS stay blurred until the visitor
     has an account: gate the viewing, not the building, so the funnel is
     "watch it get built, want it unblurred, sign up in 10 seconds" rather
     than a wall before anything's been shown. */
  .ag-pv-frame.locked iframe{filter:blur(14px) saturate(1.1);pointer-events:none;user-select:none}
  .ag-pv-gate{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:14px;padding:20px;text-align:center;z-index:6}
  /* A class setting display: flex outranks the UA's [hidden]{display:none},
     so .hidden = true silently does nothing without this — same bug class
     as .ag-type-row above. Without it the gate rendered permanently,
     including mid-build on top of the empty state. */
  .ag-pv-gate[hidden]{display:none !important;pointer-events:none !important;visibility:hidden !important}
  /* The blurred app underneath can be any colour (a dark dashboard, a white
     landing page), so neither ink nor white text is readable on its own.
     A soft wash of the page's own paper colour keeps this readable
     whatever got built — var(--paper), not a hardcoded light rgba(), so
     the wash (and the text sitting on it) is correct in dark theme too:
     the old hardcoded light wash under light --ink text in dark mode was
     nearly unreadable. */
  .ag-pv-gate::before{content:"";position:absolute;inset:0;
    background:color-mix(in srgb,var(--paper) 85%,transparent);
    backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
  .ag-pv-gate>*{position:relative}
  /* A real card, not text floating loose over the wash — reads as a
     deliberate panel instead of an accident of the blur. */
  .ag-pv-gate-card{position:relative;display:flex;flex-direction:column;align-items:center;gap:14px;
    background:var(--card);border:1px solid var(--line);border-radius:18px;
    padding:clamp(22px,5vw,32px) clamp(20px,5vw,28px);max-width:320px;
    box-shadow:0 20px 50px -12px rgba(11,22,38,.35)}
  .ag-pv-gate h3{font-family:var(--disp);font-size:1.35rem;margin:0;color:var(--ink)}
  .ag-pv-gate p{font-size:.94rem;color:var(--ink-2);margin:0;max-width:34ch;line-height:1.5}
  #agPvGateForm{display:none;flex-direction:column;gap:9px;width:100%;max-width:280px}
  #agPvGateForm.on{display:flex}
  /* :not([type=checkbox]) matters — this is an ID selector, so without it
     the terms checkbox also gets width:100% and pushes its own label into
     a one-word-wide column. The .ag-pv-gate-terms input rule below is only
     a class and loses the specificity fight. */
  #agPvGateForm input:not([type=checkbox]),#agPvGateForm select{border:1px solid var(--line);border-radius:10px;padding:11px 13px;font:inherit;font-size:.9rem;background:var(--card);color:var(--ink);width:100%}
  #agPvGateForm input:not([type=checkbox]):focus,#agPvGateForm select:focus{outline:none;border-color:var(--accent)}
  #agPvGateForm select:invalid{color:var(--mut)} /* placeholder-ish until a real country is picked */
  .ag-pv-gate-terms{display:flex;align-items:flex-start;gap:8px;text-align:left;font-size:.82rem;
    color:var(--ink-2);line-height:1.45;margin-top:2px;cursor:pointer}
  .ag-pv-gate-terms input{width:15px;height:15px;flex:none;margin-top:2px;accent-color:var(--accent);cursor:pointer}
  .ag-pv-gate-terms a{color:var(--accent);text-decoration:underline}
  .ag-pv-gate-btn{border:0;border-radius:11px;padding:12px 26px;background:var(--accent);
    color:#fff;font-weight:700;font-size:.92rem;cursor:pointer;transition:.16s;box-shadow:0 6px 18px -6px rgba(26,166,223,.7)}
  .ag-pv-gate-btn:hover{background:var(--accent-2);transform:translateY(-1px)}
  .ag-pv-gate-btn:disabled{opacity:.6;cursor:default;transform:none}
  .ag-pv-gate-err{font-size:.84rem;color:#b3261e;line-height:1.4}
  .ag-pv-gate>a{font-size:.85rem;color:var(--ink-2);text-decoration:underline}
  .ag-pv-gate>a:hover{color:var(--ink)}

  @media (max-width:900px){
    /* .ag-split is display:flex (see the "Grid before Flexbox" comment
       above) with NO flex-direction override at this width, so it kept
       its desktop default of row — .ag and .ag-preview ended up as
       side-by-side flex items instead of stacked. This used to be
       grid-template-columns:1fr, dead now that .ag-split is flex, not
       grid — flex-direction:column is what that rule was actually
       trying to say. */
    .ag-split{flex-direction:column;min-height:auto}
    .ag{border-right:0}
    .ag-log{overflow-y:visible;flex:0 0 auto;min-height:auto}
    .ag.tab-hidden,.ag-preview.tab-hidden{display:none !important}
    .nav {
      display: none !important;
    }
    /* Icon only on phones. The word "Sidebar" is wider than the 44px rail it
       sits on, so the label was covering the rail's own icons rather than
       labelling anything. Centred in the rail (7px = (44-30)/2) it reads as
       the rail's top item instead of a pill parked on top of it. */
    #agDeskSidebarToggle {
      position: fixed !important;
      top: 10px !important;
      left: 7px !important;
      z-index: 999999 !important;
      padding: 4px !important;
      width: 30px !important;
      height: 30px !important;
      justify-content: center !important;
    }
    #agDeskSidebarToggle span { display: none !important; }

    /* OPEN: mark at the far left of the header, control at the far right —
       the same arrangement as desktop. The drawer is 240px (clamped to
       80vw on very narrow screens), so the control is offset from whichever
       of those actually applies rather than from a hardcoded 240. */
    #agDeskSidebarToggle.on{left:calc(min(240px, 80vw) - 40px) !important}

    /* A real 1px divider, as the desktop rail has. Rail and page background
       are the same token, so with no border the seam between them rendered
       as an inconsistent hairline — present on some scroll positions and
       device pixel ratios, absent on others. An explicit border is the fix;
       leaving it to sub-pixel rounding is what made it look like a glitch. */
    .ag-history:not(.on){border-right:1px solid var(--line) !important}

    /* The drawer's overlay shadow was rgba(0,0,0,.5) — a black scrim edge
       that reads as a hard dark band against light paper. Softer, and warm
       enough to belong to the theme. */
    .ag-history.on{box-shadow:6px 0 28px -10px rgba(11,22,38,.28) !important}
    #agDeskPreviewToggle {
      position: fixed !important;
      top: 10px !important;
      right: 10px !important;
      z-index: 999999 !important;
    }
    .ag.is-new .ag-hero {
      padding-top: 20px !important;
    }
    /* The Sidebar / Preview buttons are position:fixed at the top, but the
       chat column scrolls the full height of the screen underneath them —
       so message text slid up and showed through the gap between the two
       buttons, which read as broken. This paints a solid band in the same
       paper colour across that strip (just under the buttons' own
       z-index), and .ag reserves matching room so nothing starts beneath
       it either. The band is pointer-events:none so it can't swallow taps
       meant for the buttons. */
    .ag-main::before{
      content:"";position:fixed;top:0;left:0;right:0;height:52px;
      background:var(--paper);z-index:999998;pointer-events:none;
    }
    .ag{padding-top:52px}

    /* Composer sits on the BOTTOM of the column once a conversation has
       started. .ag-log is flex:0 0 auto here (it has to size to its own
       content, see the note further down), so with a short conversation
       there is leftover space in .ag and the composer was landing in the
       middle of the screen with dead space under it. margin-top:auto
       absorbs exactly that leftover space and does nothing once the log
       is long enough to fill the column, so it can't push anything off.
       Scoped to :not(.is-new) because on the new-chat screen .ag-hero is
       a real flex container that deliberately CENTERS the greeting +
       composer group — pushing the composer down there would break it. */
    /* PINNED, not placed.

       Four attempts at this bug all worked the same way: keep the section a
       viewport-height flex column and trust the composer to land on its
       bottom edge. Each one fixed a real hole — a flag not cleared, a log
       whose children were not what a selector assumed, a hero left as a
       padded column — and each time the composer still floated, because the
       chain has too many links and only one of them has to be wrong.

       position:fixed removes the chain. The composer is bolted to the bottom
       of the viewport whenever a conversation exists, and no amount of wrong
       height, wrong display or stale class anywhere above it can move it.
       The log is given matching room underneath so the last line is never
       hidden behind it.

       It starts at var(--rail) because .ag-main reserves that much for the
       icon rail, plus a small inset — see the left/right declarations. */
    /* THE one that broke the phone layout for months of screenshots.

       .ag-split.has-preview .ag{flex:0 0 clamp(340px,34vw,460px)} is a WIDTH:
       it sizes the chat column beside the preview in the desktop row layout.
       It carries no media query, and down here .ag-split is a flex COLUMN --
       so the same declaration becomes a fixed 340px HEIGHT. At 352px the
       clamp floors at 340 and the chat section is locked to 340px tall in a
       608px viewport, with flex-grow:0 refusing to fill and flex-basis
       beating the height:100dvh set below.

       has-preview is added by loadProjectPreview(), which is why this only
       ever appeared after OPENING A PROJECT and never in a synthetic log.
       Every earlier fix was aimed at the log and the composer inside a
       container that was itself 268px too short. */
    .ag-split.has-preview .ag{flex:1 1 auto !important}

    .ag:not(.is-new) .ag-composer,
    .ag.has-convo .ag-composer,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-composer {
      position: fixed;
      /* var(--rail) alone was 20px short at each end.

         The rail is the easy half and it was already right: .ag-main
         reserves var(--rail) on a phone, the rail itself is position:fixed
         and overlays that reservation, so the chat column genuinely starts
         at 44. What was missing is .ag's OWN horizontal padding, which every
         message is inset by and the bar was not. So the bar ran 20px wider
         than the conversation at each end — hard against the icon rail on
         one side and against the screen edge on the other, while the
         bubbles above it sat neatly inside both.

         margin:0 auto below was an earlier attempt at this same symptom and
         could never fix it: max-width never binds at phone widths, so there
         is no free space for auto to distribute.

         HALF .ag's padding, not all of it, and that is measured rather than
         chosen. .ag-brow needs about 234px to keep the attach button, both
         pills, the mic and send on one row; insetting the full 20px leaves
         it 228 and the send button drops to a second row, making the bar
         34px taller to buy 20px of symmetry. 10px clears it with room and
         still reads as inset from both the rail and the screen edge, which
         is the whole point. The bar sitting a little wider than the bubbles
         is ordinary for a composer; sitting hard against one edge is not. */
      left: calc(var(--rail) + 10px);
      right: 10px;
      /* Lifted, like the desktop rule — plus the home-indicator inset, so
         the gap is a real gap on a phone rather than one the system UI
         sits in. */
      bottom: calc(10px + env(safe-area-inset-bottom, 0px));
      top: auto;
      /* width:auto is load-bearing. .ag-composer shares a rule with .ag-log
         and .ag-head that sets width:100%, and on a FIXED element that
         resolves against the viewport rather than the space between left and
         right — so the bar came out 352px wide starting at x=44 and carried
         the send button 44px off the right edge. left and right have to be
         what sizes it. */
      width: auto;
      z-index: 40;
      /* auto, not 0: same over-constraint as the desktop rule above, and
         with margin:0 the bar sat hard against the icon rail while the
         messages were centred in what was left. */
      margin: 0 auto;
      /* The safe-area inset moved to `bottom` above, so it is not also
         padded for here — it would be counted twice and leave a bar half
         a home indicator too tall. */
      padding: 8px 10px 8px;
      /* Fill and edge come from the floating-bar rules above — this was a
         solid var(--paper) and a 1px rule, which is exactly the floor the
         log is no longer supposed to hit. */
    }
    /* FOLLOW THE RAIL OUT.

       During a build the rail gets out of the way completely — .focus takes
       it to width:0 and translates it off-screen — and .ag-main drops the
       padding that was reserving its column, so the conversation widens by
       var(--rail) and slides left into the space.

       Every message did that and the composer did not. It is position:fixed
       with a constant left, so it cannot inherit a padding change the way
       the log does: the bubbles moved 44px left, the bar stayed where it
       was, and for the whole length of a build they were out of line with
       each other. The one moment someone is watching the screen closely is
       the one where the two halves disagreed.

       Transitioned on the rail's own duration and curve so the bar travels
       WITH the panel rather than snapping to its new place after it.

       #agMain rather than .ag-main, and that is specificity rather than
       taste. One of the three selectors this has to beat is
       `.ag:has(#agLog > *:not(.ag-empty)) .ag-composer`, and :has() takes
       the specificity of its most specific argument — so that ID inside it
       scores the whole rule (1,2,0) and quietly outranks any number of
       classes. Written with .ag-main this lost at (0,4,0) and the bar sat
       still while the messages moved, which looked exactly like the rule
       not existing. */
    .ag-history.focus ~ #agMain .ag-composer { left: 10px; }
    .ag:not(.is-new) .ag-composer,
    .ag.has-convo .ag-composer,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-composer {
      transition: left var(--sb-dur, .26s) var(--sb-ease, ease-in-out);
    }
    @media (prefers-reduced-motion: reduce) {
      .ag:not(.is-new) .ag-composer,
      .ag.has-convo .ag-composer,
      .ag:has(#agLog > *:not(.ag-empty)) .ag-composer { transition: none; }
    }

    /* The preview takes the whole screen on a phone and the chat is hidden
       behind it — but a fixed element does not care that its ancestor is
       display:none unless it is told to. It is. */
    .ag.tab-hidden .ag-composer { display: none !important; }
    /* Exactly the bar's height, measured, rather than a guessed 124px that
       was 16px too much on a short screen and would be too little the moment
       the textarea grew to two lines. --composer-h is kept up to date by a
       ResizeObserver; the fallback only matters for the first paint. */
    .ag:not(.is-new) #agLog,
    .ag.has-convo #agLog,
    .ag:has(#agLog > *:not(.ag-empty)) #agLog {
      padding-bottom: calc(var(--composer-h, 108px) + 26px + env(safe-area-inset-bottom, 0px));
    }

    /* Messages sit ON the composer, the way a chat does, instead of stacking
       from the top and leaving a field of nothing between the last line and
       the box you type in. margin-top:auto on the first child eats the free
       space when the conversation is short and computes to nothing once it
       overflows, so scrolling still behaves. */
    .ag.has-convo #agLog,
    .ag:not(.is-new) #agLog { display: flex; flex-direction: column; }
    .ag.has-convo #agLog > *:first-child,
    .ag:not(.is-new) #agLog > *:first-child { margin-top: auto; }
    /* Each of these gets a :has() twin for the same reason the desktop rules
       did: opening a project from the sidebar restores its history, and if
       .is-new has not come off by the time that paints, the phone layout —
       fixed height, hidden overflow, a log that scrolls inside itself — is
       skipped entirely and the composer floats wherever the page happens to
       be scrolled. The log's contents cannot be out of date; the flag can. */
    .ag:not(.is-new),
    .ag.has-convo,
    .ag:has(#agLog > *:not(.ag-empty)){height:100dvh;min-height:0;overflow:hidden !important}
    /* !important, and it has to be.

       .ag.is-new .ag-log sets overflow:hidden!important and flex:0 0 auto for
       the new-chat screen, where the log is a short block that must not
       scroll. Those two are exactly wrong once a conversation exists: the log
       sizes itself to its content and then CLIPS it, so the last message is
       cut off mid-line and the log's box stops short of the composer with a
       band of empty page between them.

       Without !important here that rule keeps its overflow no matter how
       specific this selector is, so every fix upstream — clearing is-new,
       setting has-convo — only mattered if it happened before paint. This
       wins outright instead. */
    .ag:not(.is-new) .ag-log,
    .ag.has-convo .ag-log,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-log{
      flex:1 1 auto !important;
      min-height:0 !important;
      overflow-y:auto !important;
      overflow-x:hidden !important;
      scrollbar-width:thin !important;
    }
    /* The new-chat screen also hides the log's scrollbar outright; give it
       back, or a conversation scrolls with no indication that it can. */
    .ag:not(.is-new) .ag-log::-webkit-scrollbar,
    .ag.has-convo .ag-log::-webkit-scrollbar,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-log::-webkit-scrollbar{
      display:block !important;width:6px !important}
    /* The hero is a padded flex column on the new-chat screen; with a
       conversation it must get out of the way so the composer is a direct
       flex child of .ag and lands on the bottom edge. */
    /* display:contents, and it must win.

       #agLog lives INSIDE #agHero, not directly under section.ag. So making
       the log flex:1 1 auto only fills the HERO -- and .ag.is-new .ag-hero is
       a padded, centred flex column sized to its content, which is why the
       log stopped a third of the way up the screen with a band of empty page
       under it no matter what the log rules said. The hero has to get out of
       the way first, so the log becomes a direct flex child of .ag and can
       fill a 100dvh column. */
    .ag:not(.is-new) .ag-hero,
    .ag.has-convo .ag-hero,
    .ag:has(#agLog > *:not(.ag-empty)) .ag-hero{display:contents !important}
    /* The device-size picker needs room to be worth having, and a phone
       has none — but 900px was the wrong line to draw it at. An iPad in
       portrait is 768 or 820 and lands in here, where the preview takes
       the WHOLE screen: there is more room for a mockup than on any
       laptop. Hiding the picker there did not simplify anything, it took
       the only control away and left whatever frame was already on — pick
       iPad on a laptop, rotate an iPad into portrait, and the preview was
       stuck on a shape with no way back.

       650 is the line the JS already draws (see applyDeviceFrame, which
       forces the phone frame below it). The two disagreeing is what
       created the 650-900px gap where a frame could be set and not
       unset. #agDeskPreviewToggle stays visible at every width here:
       it's the ONLY way to switch panes on mobile now (see its click
       handler), not a desktop-only control. */
    @media (max-width:649px){
      #agPvDevices{display:none !important}
    }
    /* BELOW the band, not on it.

       .ag-main::before (see its rule above) paints a solid 52px strip of
       paper right across the top of the screen so chat text scrolling
       under the fixed Sidebar/Preview buttons cannot show through between
       them. The picker's own top:6px puts it inside that strip, where it
       was drawn but never seen — laid out, hit-testable, and painting
       nothing. Raising its z-index above the band's does not fix it: the
       band is a fixed-position layer and wins the paint whatever the two
       numbers say. So the control moves out from under it instead, and
       the stage reserves the matching room below (see its padding-top). */
    .ag-pv-devices{top:58px}
    /* Centred over the STAGE, which is inset by the icon rail — the same
       22px correction the stage comment below explains for the mockup. */
    body.preview-mode .ag-pv-devices{left:calc(50% + 22px)}
    /* One scroll on mobile (.ag's), not a nested one — but overflow:visible
       alone was not enough. .ag-log kept flex:1 + min-height:0, so its BOX
       stayed short while its content spilled out and painted straight over
       the composer below it: the input rendered mid-conversation with
       messages appearing underneath it. It has to size to its content for
       the composer to sit after it, so flex and min-height are reset too. */
    .ag-log{overflow-y:visible;flex:0 0 auto;min-height:auto}
    .ag.tab-hidden,.ag-preview.tab-hidden{display:none !important}
    .ag-preview {
      position: relative !important;
      height: auto !important;
      padding-top: 52px !important;
      box-sizing: border-box !important;
    }
    .ag-pv-stage {
      /* 52px top: the device picker sits at top:58px, just clear of the
         band above it, and this is what keeps the mockup from starting
         underneath it. The frames measure this box (100cqh), so they take
         the room out of their own height without any further arithmetic. */
      padding: 52px 0 16px !important;
      min-height: calc(100svh - 110px) !important;
      display: flex; align-items: center; justify-content: center;
    }
    /* One column, so the device fills the screen: the 580px cap the base
       rules carry is lifted and the stage's own box becomes the limit.
       Everything else — the ratio, and fitting the axis that is NOT
       driving — is already right up there, so these only drop the cap. */
    .ag-pv-frame.mobile {
      height: min(100cqh, calc((100cqw - 8px) * 19.5 / 9)) !important;
      max-height: none;
      width: auto;
      max-width: none;
      aspect-ratio: 9 / 19.5;
      padding: 9px;
      border-radius: 46px;
    }
    .ag-pv-frame.mobile .ip-screen{border-radius:36px}
    .ag-pv-frame.mobile .ip-screen::before{width:100px;height:28px;top:10px}
    /* FILL THE STAGE, BUT STAY A DEVICE.

       These two were width:100% with an explicit height — which is both
       axes pinned, so aspect-ratio had nothing left to do. On an iPad the
       iPad mockup came out as a full-bleed rounded slab: no bezel down
       either side, the home bar off the bottom edge, the camera the only
       thing left of the frame. The MacBook came out worse — a lid taller
       than it is wide, portrait, with a base under it.

       Same shape as the phone rule above instead: one axis is the lesser
       of what the stage's height and its width allow, and aspect-ratio
       derives the other. */
    .ag-pv-frame.tablet{width:auto;max-width:none;max-height:none;
      height:min(100cqh, calc((100cqw - 16px) * 4 / 3))}
    .ag-pv-frame.desktop{height:auto;max-height:none;
      width:min(calc(100cqw - 16px), calc((100cqh - 44px) * 16 / 9))}
    body.preview-mode{height:100dvh;overflow:hidden !important}
    body.preview-mode .ag-main{height:100dvh;overflow:hidden !important}
    body.preview-mode .ag-preview{position:fixed !important;inset:0 !important;width:100% !important;height:100dvh !important;
      min-height:0 !important;padding-top:52px !important;overflow:hidden !important;background:var(--paper-2)}
    /* Inset by the rail, not by nothing.

       The stage spans the whole viewport while the icon rail is
       position:fixed over its left 44px, so centring inside the stage
       parked the phone half a rail to the left of the space anyone can
       actually see. Measured: frame centre 390, visible centre 412. */
    body.preview-mode .ag-pv-stage{height:calc(100dvh - 52px) !important;min-height:0 !important;max-height:none !important;
      padding:52px 0 12px !important;overflow:hidden !important;
      margin-left:44px !important;width:calc(100% - 44px) !important}

    /* No frame overrides here any more, and that is the fix.

       This block used to re-state each frame's size against the VIEWPORT
       — height:calc(100svh - 110px) out of preview mode, height:100% and
       (100vw - 44px) in it — because the stage is a different box in the
       two modes and percentages could only see one axis of it. Every one
       of those numbers is now a container unit read off the stage itself,
       so the rules above are already correct in both modes.

       Restating them here is what broke the iPad: height:100% pinned the
       second axis again and put the slab back the moment the preview took
       over the screen. The frames fill this stage because it is the stage
       they measure, not because anything here says so. */
  }
  @media (max-width:640px){
    .ag-say,.ag-card{padding-inline-start:0;margin-inline-start:0}
    .ag{min-height:calc(100svh - 90px)}
    /* label + device toggle + Publish + Open-in-new-tab all in one row
       don't fit at phone widths — found live, the last one was landing
       flush against the screen edge with zero padding. flex-wrap on the
       bar (base rule) is the safety net; this is what keeps it to one
       line in the common case instead of relying on the wrap. */
    .ag-pv-publish{right:96px;padding:0 10px;font-size:.78rem}
    /* Recent-project cards: auto-fill's 258px minimum only ever leaves
       room for a single column on a phone. Forcing 2 columns keeps the two
       most recent builds visible side by side instead of stacked full-width;
       the card scales itself, in project-card.css. */
    .ag-recent-grid{grid-template-columns:repeat(2,1fr);gap:10px}

  }

  /* Account sidebar. This block is a verbatim copy of the sidebar CSS on
     /projects and /deployments — the three pages now render the same rail
     from the same rules, so a change to one is a change to all three. The
     builder's own additions are kept OUTSIDE it rather than woven through
     it: the app shell immediately below, and the upgrade card plus the
     .focus building state at the end. That separation is the point — the
     previous version threaded :not(.focus) through every rail selector,
     which is why the two could never be diffed against each other. */
  /* Fixed app shell: the page itself never scrolls, only the chat log and
     the preview stage do. Everything else — header, composer, sidebar,
     preview toolbar — stays put. Scoped to >900px; below that the layout
     is single-column with Chat/Preview tabs and ordinary page scroll is
     the right behavior. */
  @media (min-width:901px){
    html,body{height:100%;overflow:hidden}
    .ag-main{height:100%;display:flex;flex-direction:column}
  }
  body{display:flex;align-items:stretch}
  .ag-main{flex:1;min-width:0}

  /* ---- sidebar: the same rail as the builder and /deployments ---- */
  .ag-history{position:relative !important;height:100vh;flex:0 0 0px;
    width:0;min-width:0;max-width:var(--sb-w);overflow:hidden;
    background:var(--paper);border-right:0 solid var(--line);box-sizing:border-box;
    transition:width .26s var(--ease),flex-basis .26s var(--ease),border-color .26s var(--ease);
    display:flex;flex-direction:column;z-index:100}
  .ag-history.on{width:var(--sb-w) !important;flex:0 0 var(--sb-w) !important;min-width:var(--sb-w) !important;border-right:1px solid var(--line);overflow:visible !important}
  .ag-hist-head,.ag-hist-tabs,.ag-hist-foot{width:100%;box-sizing:border-box}
  /* --- header: logo left, controls right --- */
  .ag-hist-head{display:flex;align-items:center;gap:8px;padding:10px 10px 12px 12px;width:100%;box-sizing:border-box}
  .ag-hist-mark{width:28px;height:28px;display:grid;place-items:center;flex:none}
  .ag-hist-mark img{width:23px;height:23px;display:block;object-fit:contain;border-radius:5px}
  /* Sits between the mark and the toggle, the way the reference does.
     margin-left:auto pushes it to the right end of the header; the right
     margin is the width the fixed toggle occupies at the very edge. */
  .ag-hist-search{margin-left:auto;margin-right:38px;flex:none;width:30px;height:30px;
    display:grid;place-items:center;border:1px solid transparent;border-radius:9px;
    background:none;color:var(--ink-2);cursor:pointer;transition:background .15s ease,color .15s ease}
  .ag-hist-search:hover{background:var(--paper-2);color:var(--ink)}
  .ag-hist-search svg{width:16px;height:16px}
  .ag-hist-brand-text{display:none;flex-direction:column;line-height:1.05;font-family:var(--disp);font-weight:800;font-size:.88rem;color:var(--ink);letter-spacing:-.01em;margin-left:6px}
  .ag-hist-brand-text span{display:block}
  .ag-ws-wrap{position:relative}
  .ag-ws{display:flex;align-items:center;gap:9px;border:1px solid var(--line);background:var(--card);color:var(--ink) !important;border-radius:999px;padding:6px 10px 6px 6px;margin:8px 10px 8px;width:calc(100% - 20px);cursor:pointer;text-align:left;box-sizing:border-box}
  /* color:#fff is load-bearing: .ag-ws svg paints every icon in the pill
     --ink-2 for the chevron, and that blanket rule was catching the avatar
     glyph too, drawing it grey on the blue circle. */
  .ag-ws .av svg,.ag-user-main .av svg{width:13px;height:13px;color:#fff}
  .ag-ws .av{flex:none;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent-2));font-weight:600;font-size:.8rem}
  .ag-ws .nm{color:var(--ink) !important;flex:1;min-width:0;font-size:.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .ag-ws svg{width:16px;height:16px;flex:none;color:var(--ink-2)}
  .ag-ws .chev{transition:transform .16s var(--ease)}
  .ag-ws.open .chev{transform:rotate(180deg)}
  .ag-ws-menu{position:absolute;left:10px;right:10px;top:calc(100% - 2px);z-index:60;
    background:var(--card);border:1px solid var(--line);border-radius:14px;padding:8px;
    box-shadow:0 16px 40px -10px rgba(11,22,38,.32);
    opacity:0;visibility:hidden;transform:translateY(-4px);
    transition:opacity .16s ease,transform .16s ease,visibility .16s ease}
  .ag-ws-menu.open{opacity:1;visibility:visible;transform:none}
  .ag-ws-signed{padding:8px 10px 10px;font-size:.78rem;color:var(--mut);border-bottom:1px solid var(--line);margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .ag-ws-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:9px;font-size:.88rem;color:var(--ink);transition:.13s;text-decoration:none;border:0;background:none;width:100%;text-align:left;cursor:pointer}
  .ag-ws-item:hover{background:var(--paper-2)}
  .ag-ws-item svg{width:15px;height:15px;flex:none}
  .ag-ws-sep{height:1px;background:var(--line);margin:6px 4px}
  .ag-act{display:flex;align-items:center;gap:9px;margin:0 10px 8px;padding:8px 10px;border:1px solid var(--line);background:var(--card);border-radius:9px;color:var(--ink);font-size:.88rem;font-weight:500;cursor:pointer;width:calc(100% - 20px);box-sizing:border-box}
  .ag-act svg{width:16px;height:16px;flex:none;color:var(--ink-2)}
  /* ---- nav rows: the builder's shape (18px icon slot, compact row) ---- */
  .ag-hist-tabs{display:flex;flex-direction:column;gap:2px;padding:2px var(--sb-pad) 4px;width:100%;box-sizing:border-box;flex:0 0 auto;overflow:visible !important}
  .ag-hist-tab{width:100%;box-sizing:border-box;display:flex;align-items:center;gap:10px;border:0;background:none;color:var(--ink);cursor:pointer;
    padding:7px 6px;border-radius:8px;font-size:.75rem;font-weight:450;text-align:left;transition:background .14s,color .14s;text-decoration:none}
  .ag-hist-tab .tab-ic{flex:none;width:18px;height:18px;display:grid;place-items:center;color:var(--ink-2)}
  .ag-hist-tab .tab-ic svg{width:16px;height:16px}
  .ag-hist-tab .tab-ic.ring{border:1px solid var(--line);border-radius:50%;background:var(--paper-2)}
  .ag-hist-tab:hover{background:var(--paper-2)}
  .ag-hist-tab.on{background:var(--paper-2);font-weight:600}
  .ag-hist-tab.on .tab-ic,.ag-hist-tab:hover .tab-ic{color:var(--ink)}
  /* Labels reveal by max-width + opacity rather than display:none, so the
     rail's open/close reads as one continuous motion. Toggling display
     makes the text pop in at full size the instant the width starts
     animating — nothing is broken, the label simply was not part of the
     movement. */
  .ag-hist-tab .lbl,.ag-user-main .nm,.ag-ws .nm,.ag-hist-brand-text{
    max-width:190px;opacity:1;overflow:hidden;white-space:nowrap;
    transition:max-width .26s var(--ease-sb),opacity .18s ease .06s}
  .ag-tab-new{background:var(--card);box-shadow:0 1px 2px rgba(0,0,0,.05)}
  .ag-tab-new:hover{background:var(--card);filter:brightness(1.06)}
  .ag-tab-new .tab-ic.ring{color:var(--ink);background:var(--paper-2)}

  /* Recent deployments take the flexible middle, the way chat history does
     in the builder, so the account row stays pinned to the bottom. */
  /* The list and the plan card kept taking turns being the casualty.

     First it was the list: flex:1 1 auto against a foot that would not
     shrink, so every pixel the plan card wanted came out of the history,
     and once the card grew (an alert, three meters, an upgrade button)
     that took it to zero — the "Recent" heading with nothing under it.
     Giving the list a min-height floor only moved the damage: the foot
     became the loser instead, and because it was also overflow:hidden it
     lost SILENTLY — clipped mid-meter, Upgrade button below the cut, no
     scrollbar to admit anything was missing.

     Neither is clipped now. The foot is always whole; the list takes what
     is left and scrolls inside itself; and the list's floor is applied
     only at window heights where that floor can actually be honoured (the
     vertical budget below). An unmeetable min-height is precisely what
     pushed the plan card off the bottom of the rail. */
  .ag-hist-chats{flex:1 1 auto;min-height:0;
    display:flex;flex-direction:column;padding:2px 0 4px}
  .ag-chats-ttl{font-size:.655rem;font-weight:450;color:var(--mut);padding:10px 10px 4px;flex:0 0 auto}
  /* No scrollbar-width / scrollbar-color on this rule, deliberately.
     Chromium honours the standard properties OVER ::-webkit-scrollbar, so
     setting them made the six-pixel rules below dead code and handed the
     rail a ten-pixel standard scrollbar instead — a visible bar eating
     sidebar width to scroll three recent projects. Measured in this
     browser: these webkit rules alone give the 6px asked for, adding
     scrollbar-width:thin widens it to 10px, and scrollbar-color without a
     width falls all the way back to the 15px default with arrow buttons.

     The thumb is transparent until the rail is hovered, so at rest nothing
     shows at all. The 6px gutter stays reserved either way, which is what
     stops the list shifting sideways when the thumb appears. */
  .ag-chats-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:0 var(--sb-pad) 4px;display:flex;flex-direction:column;gap:2px}
  .ag-chats-list::-webkit-scrollbar{width:6px;height:6px}
  .ag-chats-list::-webkit-scrollbar-track{background:transparent}
  .ag-chats-list::-webkit-scrollbar-thumb{background:transparent;border-radius:99px}
  .ag-history:hover .ag-chats-list::-webkit-scrollbar-thumb{background:var(--line)}
  .ag-chats-list::-webkit-scrollbar-thumb:hover{background:var(--mut)}
  /* Firefox has no ::-webkit-scrollbar at all, so without this it would get
     a full-width classic bar — the very thing being removed above. Scoped
     to Firefox so it cannot re-break Chromium. */
  @supports (-moz-appearance:none){
    .ag-chats-list{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
  }
  /* Rows keep their own height. A flex column shrinks its children before
     it overflows, and these had overflow:hidden so their min-content height
     was zero — 20 projects did not make the list scroll, they made every
     row 14px instead of 30 and squeezed the text inside it. The list looked
     merely "dense"; nothing announced that the layout had run out of room.
     Scrolling is the correct answer to too many rows, and it only happens
     once the rows refuse to shrink. Applies to the empty and loading
     states too, which were being flattened the same way. */
  .ag-chats-list > *{flex:0 0 auto}
  .ag-chat-item{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;
    padding:7px 6px;border-radius:8px;color:var(--ink-2);text-decoration:none;font-size:.75rem;
    white-space:nowrap;overflow:hidden;transition:background .14s,color .14s;border:0;background:none;cursor:pointer;text-align:left;font-family:inherit}
  .ag-chat-item:hover{background:var(--paper-2);color:var(--ink)}
  .ag-chat-item.current{background:var(--paper-2);color:var(--ink);font-weight:600}
  .ag-chat-item i{flex:none;width:18px;height:18px;display:grid;place-items:center;color:var(--ink-2);opacity:.95}
  .ag-chat-item i svg{width:16px;height:16px}
  .ag-chat-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
  /* A dot instead of an icon for anything currently live, so the rail shows
     what is running without needing the page. */
  .ag-chat-item .sdot{width:6px;height:6px;border-radius:50%;background:var(--mut);flex:none;margin:0 6px}
  .ag-chat-item .sdot.run{background:var(--ok)}
  .ag-chat-item .sdot.move{background:var(--warn)}
  .ag-chat-item .sdot.bad{background:var(--danger)}
  .ag-chats-empty{padding:14px;color:var(--mut);font-size:.8rem;line-height:1.5;text-align:center}

  /* ---- plan block, same shape as the one on /projects ----
     The numbers here are real rather than the static placeholders that
     page ships: apps created against the free allowance, agent spend
     against the monthly budget, and — since this is the deployments
     page — host capacity as the cloud meter, which is the thing that
     actually decides whether the next deploy is accepted. */
  /* Never the casualty. A plan card cut in half is worse than a short
     history: it hides the alert's consequence and the button that answers
     it. On a short window it sheds optional rows on purpose — see the
     vertical budget below — and is otherwise always drawn in full. */
  .ag-hist-foot{flex:0 0 auto;
    padding:8px 10px 8px;border-top:1px solid var(--line);background:var(--paper);
    width:100%;box-sizing:border-box}
  .ag-hist-foot .ttl{font-size:.84rem;font-weight:600;padding:0 2px;margin-bottom:4px;color:var(--ink)}
  .ag-plan-alert{background:rgba(239,68,68,.15);border:1px solid rgba(239,68,68,.25);color:#f87171;font-size:.76rem;
    font-weight:500;padding:4px 8px;border-radius:6px;margin-bottom:6px;text-align:center;width:100%;box-sizing:border-box}
  .ag-meter-row{display:flex;align-items:center;gap:7px;padding:2px 0;width:100%;box-sizing:border-box}
  .ag-meter-row svg{width:14px;height:14px;flex:none;color:var(--ink-2)}
  .ag-meter-row .lab{flex:1;min-width:0;font-size:.78rem;color:var(--ink);font-weight:500}
  .ag-meter-row .lab i{display:block;font-style:normal;color:var(--mut);font-size:.70rem;font-weight:400;margin-top:0}
  .ag-meter{width:54px;height:5px;border-radius:999px;background:rgba(128,128,128,.18);overflow:hidden;flex:none}
  .ag-meter i{display:block;height:100%;border-radius:999px;background:#3b82f6;transition:width .4s var(--ease)}
  .ag-meter i.warn{background:var(--warn)}
  .ag-meter i.hot{background:var(--danger)}
  .ag-upgrade{display:flex;align-items:center;justify-content:center;gap:7px;margin-top:8px;padding:9px 12px;
    border:0;border-radius:9px;background:#0077ff;color:#fff;font-size:.9rem;font-weight:600;
    cursor:pointer;width:100%;box-sizing:border-box;text-decoration:none;transition:.15s}
  .ag-upgrade:hover{background:#0062cc}
  .ag-hist-install{display:flex;align-items:center;justify-content:center;gap:5px;margin-top:6px;font-size:.72rem;color:var(--mut);width:100%;box-sizing:border-box}
  .ag-hist-install svg{width:13px;height:13px;color:var(--ink-2);flex:none;transition:color .15s ease,transform .15s ease;cursor:pointer}
  .ag-hist-install svg:hover{color:#0077ff !important;transform:scale(1.15)}
  .ag-hist-install a:hover{color:var(--ink)}
  .ag-hist-links{display:flex;gap:8px;justify-content:center;margin-top:5px;font-size:.71rem;color:var(--mut);width:100%;box-sizing:border-box}
  .ag-hist-links a:hover{color:var(--ink)}

  /* ---- vertical budget -------------------------------------------------
     Header, workspace pill, nav and account row cost ~300px before either
     the history or the plan card gets a single pixel, and the card itself
     is ~200px once the usage alert is up. Under roughly 600px of window
     the two cannot both be whole, so the rail gives something up
     deliberately here rather than leaving it to whichever flex rule
     happened to win.

     Room for both      → the history gets a real floor.
     Getting tight      → the card drops its footer links and puts each
                          meter's number back on the label's own line.
     Shorter than that  → no floor at all: the history shrinks toward its
                          scrollbar and the card stays whole, because a
                          half-drawn meter reads as breakage while a short
                          list reads as a short list. */
  @media (min-height:660px){ .ag-hist-chats{min-height:clamp(96px,18vh,260px)} }
  @media (min-height:560px) and (max-height:659px){ .ag-hist-chats{min-height:76px} }
  /* Compressed, not deleted.

     These two rows were display:none below 700px, and 700px of viewport is
     more than a laptop usually has once the browser's own chrome is taken
     off the screen — so "Install Souqi on ... Changelog" and the
     Terms / Privacy / Plans line were simply absent for most people, on
     every page with this rail. They are two 14px rows: the room comes from
     tightening what sits around them and letting the chat list give up a
     little more of its minimum, which it can afford because it scrolls. */
  @media (max-height:700px){
    .ag-hist-install{font-size:.68rem;gap:4px;margin-top:5px}
    .ag-hist-install svg{width:12px;height:12px}
    .ag-hist-links{font-size:.67rem;gap:7px;margin-top:4px}
    .ag-hist-chats{min-height:56px}
    .ag-hist-foot{padding-top:8px;padding-bottom:8px}
    .ag-meter-row{padding:2px 0}
  }
  @media (max-height:620px){
    .ag-meter-row .lab i{display:inline;margin-left:6px}
    .ag-meter-row{padding:1px 0}
    .ag-upgrade{margin-top:6px;padding:7px 12px}
    .ag-hist-foot{padding-top:6px;padding-bottom:6px}
  }

  /* Account row pinned to the very bottom: avatar, name, settings gear. */
  .ag-hist-user{flex:0 0 auto;display:flex;align-items:center;gap:2px;padding:5px var(--sb-pad) 8px;
    background:var(--paper-2);width:100%;box-sizing:border-box}
  .ag-user-main{flex:1;min-width:0;display:flex;align-items:center;gap:8px;border:0;background:none;
    color:var(--ink);cursor:pointer;padding:5px 6px;border-radius:9px;font:inherit;text-align:left;transition:.14s}
  .ag-user-main:hover{background:var(--paper)}
  .ag-user-main .av{flex:none;width:22px;height:22px;border-radius:50%;display:grid;place-items:center;
    background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#fff;font-size:.76rem;font-weight:700}
  .ag-user-main .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.72rem;font-weight:500}
  .ag-user-gear{flex:none;width:24px;height:24px;display:grid;place-items:center;border:0;background:none;
    color:var(--mut);cursor:pointer;border-radius:9px;transition:.14s}
  .ag-user-gear:hover{background:var(--paper);color:var(--ink)}
  .ag-user-gear svg{width:14px;height:14px}

  .ag-pv-toggle-btn{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);background:var(--card);color:var(--ink);
    border-radius:9px;padding:6px 14px;height:34px;font-size:.85rem;font-weight:600;cursor:pointer;
    transition:all .2s var(--ease);box-shadow:0 2px 6px rgba(0,0,0,.03);outline:none !important}
  #agDeskSidebarToggle{position:fixed !important;top:10px !important;left:10px !important;z-index:999999 !important;
    padding:5px 10px !important;height:32px !important;font-size:.82rem !important}
  .ag-pv-toggle-btn:hover{border-color:#cbd5e1;background:var(--paper-2);color:var(--ink)}
  .ag-pv-toggle-btn.on{background:var(--card);color:var(--ink)}
  .ag-pv-toggle-btn .pane-left{transition:fill .2s ease;fill:transparent}
  .ag-pv-toggle-btn.on .pane-left{fill:var(--accent)}
  .ag-pv-toggle-btn svg{width:16px;height:16px;flex:none}

  /* ================= desktop rail =================
     COLLAPSED = an icon rail, not nothing. Taking the sidebar to width:0
     throws away every navigation affordance on the page — the only way
     back to Projects or the builder becomes "reopen the drawer first".
     At --rail the icons and their titles stay reachable while the content
     column gets back most of the width. */
  @media (min-width:901px){
    .ag-history{
      flex:0 0 var(--rail) !important;width:var(--rail) !important;min-width:var(--rail) !important;
      border-right:1px solid var(--line);overflow:hidden;
      transition:width .34s var(--ease-sb),flex-basis .34s var(--ease-sb),margin-right .34s var(--ease-sb),
                 border-color .34s var(--ease-sb),box-shadow .34s var(--ease-sb);
      /* Delays the CLOSE only — the hover rule zeroes it — so a pointer
         clipping the rail on its way somewhere else does not make the
         panel flash open and shut. */
      transition-delay:.16s;will-change:width;z-index:100}
    .ag-history.on{width:var(--sb-w) !important;flex:0 0 var(--sb-w) !important;min-width:var(--sb-w) !important;
      overflow:visible !important;box-shadow:none;margin-right:0}

    /* HOVER TO OPEN. The negative margin is what keeps this an OVERLAY
       rather than a push: the flex item measures --sb-w but the margin
       cancels all but --rail of it, so the content column never moves.
       Expanding it in place would reflow the whole page on every
       mouse-over, which is unusable. */
    @media (hover:hover) and (pointer:fine){
      .ag-history:not(.on):not(.hover-suppressed):hover,
      .ag-history.hover-open:not(.on):not(.hover-suppressed){
        width:var(--sb-w) !important;flex-basis:var(--sb-w) !important;
        margin-right:calc(var(--rail) - var(--sb-w));
        overflow:visible !important;z-index:400 !important;transition-delay:0s;
        box-shadow:6px 0 28px -8px rgba(11,22,38,.28)}
    }

    /* --- what the rail actually shows --- */
    .ag-history:not(.on):not(:hover):not(.hover-open) .lbl,
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-brand-text,
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-user-main .nm,
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-ws .nm{max-width:0 !important;opacity:0 !important;transition-delay:0s}

    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-ws .chev,
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chats-ttl,
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-foot{display:none !important}

    /* Settings keeps its icon on the collapsed rail. Hiding it left the one
       route into Settings only reachable by first widening the sidebar — and
       every other item in that rail survives the collapse as an icon, so the
       gear disappearing read as a missing feature rather than a tidier rail.

       Stacked, not side by side. The user row is a flex ROW, so putting the
       gear back next to the avatar asked a 44px rail to hold a 32px avatar,
       a 30px gear and the gap between them: the avatar lost, squeezed to
       12px, and the pair still ran to the rail's edge. Avatar over gear is
       how the phone rail already does it — this is the same treatment, which
       the desktop rules had simply never been given. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-user{
      flex-direction:column;align-items:center;gap:4px;
      padding-left:0 !important;padding-right:0 !important}
    /* gap:0, like the tab and workspace rows above. The name beside the
       avatar is collapsed to max-width:0 rather than removed, so it keeps
       its transition — but a zero-width flex item is still a flex item, and
       the 8px gap in front of it was being centred along with the circle.
       That pushed the avatar 4px left of the rail's centre line, out of step
       with the gear beneath it and every status dot above. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-user-main{
      flex:none;width:32px;padding:5px 0;justify-content:center;gap:0}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-user-gear{
      display:grid !important;flex:none;width:30px;height:24px}

    /* Click-opened, so letting it render inside a panel that vanishes on
       mouse-out would strand it. */
    .ag-history:not(.on) .ag-ws-menu{display:none !important}

    /* The apps stay in the rail as bare status dots — dropping them would
       make the collapsed sidebar a dead end for getting back into one. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-chats{border-top:1px solid var(--line);margin:5px 9px 0;padding-top:6px}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chats-list{padding:0;overflow:hidden}
    /* Fills the list box rather than being a fixed 32px inside it: the
       list is inset 9px each side for its hairline, so a 32px child could
       not centre in the 26px that leaves and sat 3px right. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chat-item{width:100%;margin:0;padding:7px 0;justify-content:center;gap:0}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chat-item span{display:none}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chats-empty{display:none}

    /* Height clears the fixed sidebar toggle sitting at top-left. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-head{padding:8px 0 4px !important;height:44px !important;flex:0 0 44px !important;overflow:hidden;justify-content:center}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-mark{margin:0 auto}
    /* The rail is 44px of logo. Nothing else fits, and the search box it
       would focus is not on screen there either. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-search{display:none !important}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-tabs{padding:4px 0 6px !important;align-items:center}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-tab{width:32px;padding:7px 0;justify-content:center;gap:0}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-tab-new{background:transparent;box-shadow:none}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-tab-new .tab-ic.ring{background:var(--card);border-color:var(--line);box-shadow:0 1px 2px rgba(0,0,0,.06)}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-user{background:transparent;justify-content:center}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-ws-wrap{padding:0 !important;display:flex;justify-content:center}
    /* gap:0 matters: the name is still a flex item at max-width:0, so the
       pill's 9px gap was being counted and justify-content:center pushed
       the avatar ~5px left of the rail. */
    /* No pill chrome in the rail. The white background and border make
       it read as a dropdown when the name and chevron are there; with only
       the avatar showing they are just a ring drawn around a circle. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-ws{width:32px;margin:6px auto;padding:3px;justify-content:center;gap:0;background:transparent;border-color:transparent}
    /* The dot carries side margins for the expanded row; in the rail they
       are what pushes it off centre. */
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chat-item .sdot{margin:0}


    /* Collapsed: the toggle is an icon, not a labelled button. Same as
       the builder — the word "Sidebar" beside a 44px rail is wider than
       the rail it describes. */
    #agDeskSidebarToggle:not(.on) span{display:none !important}
    #agDeskSidebarToggle:not(.on){padding:4px !important;width:30px !important;height:30px !important;
      left:calc(var(--sb-w) - 40px) !important;justify-content:center !important}
    /* Collapsed = the logo and nothing else, the way the reference does
       it. The toggle is not merely unlabelled in the rail, it is gone: a
       floating button parked beside a 44px column is the one thing there
       that is not navigation. It fades back in while the rail is hovered
       — which is also when the panel opens — so pinning it open is still
       one click, and hover-open below is what stops it collapsing out
       from under that click. */
    /* Two forms, because the button does not sit in the same place on
       every page: a direct sibling of the sidebar on /deployments and
       /projects, but nested inside .ag-main on /agent. A bare `~` matched
       nothing there and failed silently, so the toggle never hid on the
       builder while looking correct on the other two. */
    /* HIDING IT IS A HOVER FEATURE, SO IT IS SCOPED LIKE ONE.

       Only a pointer that can hover can bring the button back, so on a
       touch screen these two rules are a trapdoor: the toggle is
       opacity:0 and pointer-events:none the moment the sidebar is
       collapsed, and the hover that un-hides it never happens. On an
       iPad the sidebar could not be opened at all — measured on
       /projects, /deployments and /security, where the rail sits at 44px
       and the only control that opens it is unreachable. (The builder
       escaped it by opening the sidebar on a tap of the rail itself, and
       by starting open; the same trapdoor is in its CSS.)

       The guard is the one the hover-to-open rule above already uses, so
       the two halves of the behaviour now appear and disappear together.
       Without a hover pointer the toggle simply stays visible, which is
       what every touch device needs anyway. */
    @media (hover:hover) and (pointer:fine){
      .ag-history:not(.on) ~ #agDeskSidebarToggle,
      .ag-history:not(.on) ~ * #agDeskSidebarToggle{opacity:0;pointer-events:none;transition:opacity .18s ease}
      .ag-history:not(.on):hover ~ #agDeskSidebarToggle,
      .ag-history:not(.on):hover ~ * #agDeskSidebarToggle,
      .ag-history:not(.on).hover-open ~ #agDeskSidebarToggle,
      .ag-history:not(.on).hover-open ~ * #agDeskSidebarToggle{opacity:1;pointer-events:auto}
    }
    /* OPEN: the icon alone, at the right edge of the sidebar header —
       where the reference puts its controls. The word "Sidebar" was the
       widest thing in that row and only named what the icon already
       shows; dropping it is what lets the mark sit at the far left with
       the control at the far right instead of both bunched together.
       The .on glow stays: that, not the label, is the state cue. */
    #agDeskSidebarToggle.on span{display:none !important}
    #agDeskSidebarToggle.on{padding:4px !important;width:30px !important;height:30px !important;
      justify-content:center !important;left:calc(var(--sb-w) - 40px) !important}
    .ag-history.on .ag-hist-search{order:2;margin-left:auto;margin-right:0}
    .ag-history.on .ag-hist-mark{order:3;margin-left:0}
    .ag-history:hover .ag-hist-search,.ag-history.hover-open .ag-hist-search{order:2;margin-left:auto;margin-right:0}
    .ag-history:hover .ag-hist-mark,.ag-history.hover-open .ag-hist-mark{order:3;margin-left:0}
    #agDeskSidebarToggle:not(.on){left:7px !important}
    #agDeskSidebarToggle.on{left:7px !important}

    /* The content column sits beside the rail, so it only needs to clear
       the fixed toggle button, not the whole panel. */
    .ag-main .nav{padding-left:96px}
    .ag-history.on ~ .ag-main .nav{padding-left:clamp(18px,4vw,40px)}
  }

  @media (max-width:900px){
    .ag-history{position:fixed !important;top:0 !important;left:0 !important;bottom:0 !important;
      height:100vh !important;width:240px !important;max-width:80vw !important;z-index:999999 !important;
      transform:translateX(-100%) !important;transition:transform .28s var(--ease) !important;box-shadow:4px 0 24px rgba(0,0,0,.5)}
    .ag-history.on{transform:translateX(0) !important}

    /* CLOSED ON PHONE = the same icon rail as desktop, not nothing.
       It used to translate fully off-screen, so closing the drawer left the
       page with no navigation at all. Same argument as the desktop rail,
       same 44px.

       The element is NARROWED to the rail rather than translated most of the
       way off: translating leaves it 240px wide with only its left edge
       showing, and its contents keep laying out for the full 240px, so the
       centred icons land off the visible strip and it renders as an empty
       bar. Shrinking it collapses the inner layout, exactly as on desktop. */
    .ag-history:not(.on){
      width:var(--rail) !important;min-width:var(--rail) !important;max-width:var(--rail) !important;
      transform:translateX(0) !important;box-shadow:none !important;overflow:hidden !important;
      transition:width .28s var(--ease),transform .28s var(--ease) !important;
      /* Clears the fixed #agDeskSidebarToggle at top:10px. Its desktop
         hide-on-hover rule cannot apply here — there is no hover on touch,
         so the button must stay reachable and the rail starts below it. */
      padding-top:44px !important}

    /* position:fixed means the rail overlays rather than pushes, so the
       content needs that width back as padding. */
    .ag-main{padding-left:var(--rail)}

    /* --- rail contents: icons only (desktop's rules, minus :hover) --- */
    .ag-history:not(.on) .lbl,
    .ag-history:not(.on) .ag-hist-brand-text,
    .ag-history:not(.on) .ag-user-main .nm,
    .ag-history:not(.on) .ag-ws .nm{max-width:0 !important;opacity:0 !important;overflow:hidden !important}
    /* These are the things with no 44px form, and they are HIDDEN.
       They used to be — the declaration block that said so went missing at
       some point, and the selector list simply ran on into the next rule.
       So instead of display:none, the chevron, the search box, the
       "Recent" title, the upsell and the workspace menu were all being
       given a border-top and margins meant for .ag-hist-chats.

       The visible symptom was the account avatar hanging off the left edge
       of the rail: the pill is 32px wide, and with the chevron still in it
       the avatar had nowhere to sit but outside. The clipped "Rece" and the
       overflowing search box were the same bug wearing different clothes. */
    .ag-history:not(.on) .ag-ws .chev,
    .ag-history:not(.on) .ag-hist-view,
    .ag-history:not(.on) .ag-chats-ttl,
    .ag-history:not(.on) .ag-upsell,
    /* Same as this page's desktop rail: the search button and the mark
       both want the head's 44px, and side by side they overflow it —
       which pushed the Souqi mark off the left edge entirely. */
    .ag-history:not(.on) .ag-hist-search,
    .ag-history:not(.on) .ag-ws-menu{display:none !important}

    .ag-history:not(.on) .ag-hist-chats{border-top:1px solid var(--line);margin:5px 6px 0;padding-top:6px}
    .ag-history:not(.on) .ag-hist-chats.is-empty{border-top:0;margin:0;padding-top:0}
    .ag-history:not(.on) .ag-chats-list{padding:0;overflow:hidden}
    .ag-history:not(.on) .ag-chat-item{width:32px;margin:0 auto;padding:7px 0;justify-content:center;gap:0}
    .ag-history:not(.on) .ag-chat-item .sdot{margin:0}
    .ag-history:not(.on) .ag-chat-item span{display:none}
    .ag-history:not(.on) .ag-chats-empty{display:none}
    .ag-history:not(.on) .ag-hist-head{padding:8px 0 4px !important;height:44px !important;flex:0 0 44px !important;overflow:hidden;justify-content:center}
    .ag-history:not(.on) .ag-hist-mark{margin:0 auto}
    /* The workspace row keeps its white card look and its asymmetric
       6px/10px padding from the expanded panel, so in the rail it was the one
       icon sitting inside a visible white pill, nudged off-centre by the extra
       right padding — every other icon is transparent and centred. Desktop
       already strips it in the rail; this is the same treatment. */
    .ag-history:not(.on) .ag-ws-wrap{padding:0 !important;display:flex;justify-content:center}
    .ag-history:not(.on) .ag-ws{width:32px !important;margin:0 !important;padding:5px 0 !important;
      justify-content:center;gap:0;background:transparent !important;border-color:transparent !important}
    .ag-history:not(.on) .ag-hist-tabs{padding:4px 0 6px !important;align-items:center}
    .ag-history:not(.on) .ag-hist-tab{width:32px;padding:7px 0;justify-content:center;gap:0}
    .ag-history:not(.on) .ag-tab-new{background:transparent;box-shadow:none}
    .ag-history:not(.on) .ag-hist-user{background:transparent}
    .ag-history:not(.on) .ag-hist-user{flex-direction:column;gap:3px;padding-left:0;padding-right:0}
    .ag-history:not(.on) .ag-user-main{flex:none;width:32px;padding:5px 0;justify-content:center;gap:0}
    .ag-history:not(.on) .ag-user-gear{display:grid !important;flex:none;width:30px;height:24px}
    /* Matches this page's own desktop rail, which hides the footer entirely:
       the plan card, meters and legal links have no 44px form, and left
       visible they overflow the rail as wrapped, clipped text. */
    .ag-history:not(.on) .ag-hist-foot{display:none !important}

    /* Icon only on phones. The word "Sidebar" is wider than the 44px rail it
       sits on, so the label covered the rail's own icons rather than
       labelling anything. Centred in the rail (7px = (44-30)/2) it reads as
       the rail's top item instead of a pill parked on top of it. */
    #agDeskSidebarToggle{position:fixed !important;top:10px !important;left:7px !important;
      z-index:999999 !important;padding:4px !important;width:30px !important;height:30px !important;
      justify-content:center !important}
    #agDeskSidebarToggle span{display:none !important}

    /* OPEN: mark at the far left of the header, control at the far right —
       the same arrangement as desktop. The drawer is 240px (clamped to
       80vw on very narrow screens), so the control is offset from whichever
       of those actually applies rather than from a hardcoded 240. */
    #agDeskSidebarToggle.on{left:calc(min(240px, 80vw) - 40px) !important}

    /* A real 1px divider, as the desktop rail has. Rail and page background
       are the same token, so with no border the seam between them rendered
       as an inconsistent hairline — present on some scroll positions and
       device pixel ratios, absent on others. An explicit border is the fix;
       leaving it to sub-pixel rounding is what made it look like a glitch. */
    .ag-history:not(.on){border-right:1px solid var(--line) !important}

    /* The drawer's overlay shadow was rgba(0,0,0,.5) — a black scrim edge
       that reads as a hard dark band against light paper. Softer, and warm
       enough to belong to the theme. */
    .ag-history.on{box-shadow:6px 0 28px -10px rgba(11,22,38,.28) !important}
    .ag-hist-backdrop{display:block !important;position:fixed !important;top:0 !important;right:0 !important;bottom:0 !important;left:min(var(--sb-w),80vw) !important;
      background:var(--scrim) !important;
      backdrop-filter:var(--scrim-blur) !important;z-index:999998 !important;opacity:0 !important;pointer-events:none !important;transition:opacity .28s ease !important}
    .ag-hist-backdrop.on{opacity:1 !important;pointer-events:auto !important}
  }

  /* ---- upgrade card ----
     The builder's replacement for the three credit meters and the ✦ Upgrade
     button that /projects and /deployments carry in this same slot. Kept
     here, after the copied block, so the shared rules above stay a clean
     copy and this reads as the one deliberate difference it is. */
  /* ---- upgrade card ---- */
  .ag-upsell{margin:6px var(--sb-pad) 5px;padding:14px 11px 17px;border:1px solid var(--line);border-radius:13px;
    background:var(--card);box-shadow:0 1px 3px rgba(0,0,0,.06),0 6px 18px -10px rgba(0,0,0,.22);
    transition:background-color .22s ease}
  .up-head{display:flex;align-items:center;gap:8px;text-decoration:none;color:inherit}
  /* The tint belongs to the whole card, not its top strip. It used to live
     on .up-head — the title row only — bled out to the card edges with
     negative margins, so the card lit up across the top and stopped dead at
     the meter. That reads as a rendering fault, not a hover. */
  .ag-upsell:hover{background:rgba(26,166,223,.12)}
  .up-txt{flex:1;min-width:0}
  .up-txt b{display:block;font-size:.7rem;font-weight:600;color:var(--ink);letter-spacing:-.012em;line-height:1.25}
  .up-txt small{display:block;font-size:.625rem;color:var(--mut);margin-top:2px;line-height:1.3}
  .up-spark{flex:none;width:27px;height:27px;border-radius:9px;display:grid;place-items:center;
    background:var(--accent);color:#fff;transition:.15s;box-shadow:0 3px 10px -3px rgba(26,166,223,.6)}
  .ag-upsell:hover .up-spark{background:var(--accent-2)}
  .up-spark svg{width:14px;height:14px;fill:#fff}
  .up-meter{display:flex;align-items:center;justify-content:space-between;margin-top:13px;font-size:.655rem}
  .up-lab{color:var(--ink-2)}
  .up-pct{color:var(--ink);font-weight:600}
  .up-bar{display:block;height:6px;border-radius:999px;background:var(--paper-2);overflow:hidden;margin-top:8px}
  .up-bar i{display:block;height:100%;background:var(--accent);border-radius:999px;transition:width .4s var(--ease-sb)}

  /* ---- recent-chat filter ----
     The other builder-only addition, kept out here with the upgrade card so
     the copied block above stays a clean copy. Display:none by default, so
     with the field closed this sidebar is identical to /projects — the
     button reveals it. */
  .ag-chats-find{display:none;flex:0 0 auto;padding:0 var(--sb-pad) 6px}
  .ag-chats-find.on{display:block}
  .ag-chats-find input{width:100%;box-sizing:border-box;padding:6px 9px;font:inherit;font-size:.75rem;
    color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;outline:none;
    transition:border-color .14s ease,box-shadow .14s ease}
  .ag-chats-find input::placeholder{color:var(--mut)}
  .ag-chats-find input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(26,166,223,.14)}
  .ag-chats-find input::-webkit-search-cancel-button{cursor:pointer}
  /* Lit while the field is open, so the button reads as a toggle rather
     than something that fired once and did nothing visible. */
  .ag-hist-search.on{background:var(--paper-2);color:var(--ink)}
  .ag-chats-none{padding:14px;color:var(--mut);font-size:.75rem;line-height:1.5;text-align:center}
  /* A text field has no 44px form, and the button that opens it is already
     hidden in the rail by the copied block — so the field cannot be reached
     from there and must not be occupying space in it either. Same for the
     building state, which hides the whole panel. */
  @media (min-width:901px){
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-chats-find{display:none !important}
  }
  @media (max-width:900px){
    .ag-history:not(.on) .ag-chats-find{display:none !important}
  }

  /* ---- .focus: the building state ----
     Added to #agHistory while a build runs and removed when it finishes
     (see the is-building handling in the script). The rail gets out of the
     way completely — this is the one case where width:0 is the right
     answer, because during a build there is nothing to navigate to.
     Last in the block on purpose: these selectors are no more specific
     than the rail and hover rules above, so source order is what decides.
     :hover is spelled out because .ag-history:not(.on):hover would
     otherwise re-expand the panel the moment the pointer crossed it. */
  .ag-history.focus,
  .ag-history.focus:hover{
    width:0 !important;flex:0 0 0px !important;min-width:0 !important;max-width:0 !important;
    margin-right:0 !important;padding-top:0 !important;overflow:hidden !important;
    border-right:0 !important;box-shadow:none !important;transform:translateX(-100%) !important}
  .ag-history.focus ~ .ag-main{padding-left:0}

  .ag-hist-view{flex:1 1 auto;min-height:0;overflow-y:auto;display:none}
  .ag-hist-view.on{display:block}
  .ag-hist-view::-webkit-scrollbar{width:8px}
  .ag-hist-view::-webkit-scrollbar-thumb{background:rgba(var(--wash-rgb),.14);border-radius:8px}
  .ag-hist-view::-webkit-scrollbar-thumb:hover{background:rgba(var(--wash-rgb),.24)}

  .ag-hist-list{padding:6px 10px}
  .ag-hist-item{display:flex;align-items:center;gap:2px;width:100%;text-align:left;border:1px solid transparent;background:none;border-radius:12px;
    color:var(--ink);transition:background .15s,border-color .15s,transform .15s;margin-bottom:2px}
  .ag-hist-item:hover{background:var(--card);border-color:var(--line);box-shadow:0 2px 8px -4px rgba(11,22,38,.18)}
  .ag-hist-item.current{background:var(--tint-2);border-color:#cbe7f6}
  .ag-hist-item .hi-main{flex:1;min-width:0;display:flex;align-items:center;gap:10px;padding:8px 4px 8px 8px;cursor:pointer;background:none;border:0;text-align:left;font:inherit;color:inherit}
  /* var(--body), not var(--disp) — the sidebar is UI chrome, and Replit
     sets all of it in one neutral sans with weight doing the work. A
     display face here was the last thing in the rail still reading as a
     different typeface. */
  .ag-hist-ic{width:32px;height:32px;border-radius:9px;flex:none;display:grid;place-items:center;color:#fff;font-family:var(--body);font-weight:600;font-size:.86rem}
  .ag-hist-item .meta{display:flex;flex-direction:column;min-width:0;flex:1}
  .ag-hist-item .t{font-size:.89rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .ag-hist-item .w{font-size:.74rem;color:var(--mut);margin-top:1px}
  .ag-hist-star{flex:none;border:0;background:none;cursor:pointer;color:var(--mut);width:30px;height:30px;border-radius:8px;display:grid;place-items:center;margin-inline-end:2px;opacity:.55;transition:.15s}
  .ag-hist-item:hover .ag-hist-star{opacity:1}
  .ag-hist-star:hover{background:rgba(var(--wash-rgb),.08);color:var(--ink)}
  .ag-hist-star svg{width:15px;height:15px}
  .ag-hist-star.on{color:#e0a11a;opacity:1}
  .ag-hist-star.on svg{fill:currentColor}
  .ag-hist-empty,.ag-hist-loading{padding:30px 16px;color:var(--mut);font-size:.86rem;text-align:center;line-height:1.5}
  .ag-hist-new{margin:10px;display:flex;align-items:center;justify-content:center;gap:7px;border:0;
    background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#fff;border-radius:12px;
    padding:11px 12px;font-size:.88rem;font-weight:700;cursor:pointer;transition:.16s;box-shadow:0 4px 14px -5px rgba(26,166,223,.65)}
  .ag-hist-new svg{width:15px;height:15px}
  .ag-hist-new:hover{transform:translateY(-1px);box-shadow:0 6px 18px -5px rgba(26,166,223,.75)}
  .ag-hist-new:active{transform:translateY(0)}
  /* Hidden at every width: "Create something new" at the top of the rail
     does exactly the same thing (both go to /agent), so this was a second
     button for one action — and it sat awkwardly between the nav and the
     builds list. Must stay AFTER .ag-hist-new's own display:flex above:
     same specificity, so source order is what decides, and an override
     placed earlier did nothing at all. */
  .ag-hist-new{display:none}

  /* Stats view */
  .ag-stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:14px}
  .ag-stat{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px;position:relative;overflow:hidden}
  .ag-stat::before{content:"";position:absolute;inset:0;background:linear-gradient(135deg,rgba(26,166,223,.07),transparent 60%)}
  .ag-stat .n{font-family:var(--disp);font-size:1.7rem;font-weight:700;letter-spacing:-.02em;position:relative;
    background:linear-gradient(135deg,var(--ink),var(--accent-2));-webkit-background-clip:text;background-clip:text;color:transparent}
  .ag-stat .l{font-size:.76rem;color:var(--mut);margin-top:2px;position:relative}
  .ag-stat-types{padding:4px 14px 14px}
  .ag-stat-types h4{font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin:0 0 8px}
  .ag-stat-row{display:flex;align-items:center;gap:9px;font-size:.85rem;padding:6px 0}
  .ag-stat-row .bar{flex:1;height:7px;border-radius:4px;background:var(--paper-2);overflow:hidden}
  .ag-stat-row .bar i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent-2));border-radius:4px}
  .ag-stat-row .n{font-weight:700;width:22px;text-align:right}


  /* Settings view */
  .ag-set-profile{margin:10px;padding:16px;border-radius:16px;background:linear-gradient(135deg,var(--dark),var(--navy-800));
    color:#fff;display:flex;align-items:center;gap:12px}
  .ag-set-avatar{width:42px;height:42px;border-radius:12px;background:linear-gradient(135deg,var(--accent),var(--accent-2));
    display:grid;place-items:center;font-family:var(--disp);font-weight:700;font-size:1.1rem;flex:none}
  .ag-set-profile .plan{display:inline-flex;align-items:center;margin-top:3px;font-size:.72rem;font-weight:700;
    background:rgba(255,255,255,.14);border-radius:999px;padding:2px 9px;letter-spacing:.02em}
  .ag-set-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:13px 16px;border-bottom:1px solid var(--line)}
  .ag-set-row .l{font-size:.86rem;color:var(--ink-2)}
  .ag-set-row .v{font-size:.86rem;font-weight:600}
  .ag-set-row .bar{width:90px;height:6px;border-radius:4px;background:var(--paper-2);overflow:hidden}
  .ag-set-row .bar i{display:block;height:100%;background:var(--accent)}
  .ag-set-cta{margin:14px 16px;text-align:center;padding:11px;border-radius:11px;background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#fff;font-weight:700;font-size:.88rem;text-decoration:none;display:block;transition:.16s;box-shadow:0 4px 14px -5px rgba(26,166,223,.65)}
  .ag-set-cta:hover{transform:translateY(-1px)}

  /* Settings overlay (see the markup note at the top of <body>) —
     Replit's shape: a large centred modal over a dimmed scrim that
     covers the whole workspace, not a side panel. */
  /* Scrim: a neutral dim plus a real blur, not a colour wash. It used to be
     rgba(11,22,38,.75) — a cold navy at 75% — which on this warm dark
     palette repainted the whole workspace a different colour rather than
     just pushing it back. Near-black at 32% keeps the page's own colour
     recognisable underneath, and the blur (not the opacity) is what does
     the separating. */
  /* ---------- build confirmation card ---------- */
  /* ═══════ THE PLAN CARD ═══════
     Plan mode's one piece of UI, and the only thing standing between
     someone and their app, so it has to look like the moment it is: a
     considered proposal, not a system dialog. It was a grey bordered box
     with a flat list in it, which is what every confirm step has looked
     like for fifteen years.

     The lit rim is the composer's own treatment, reused deliberately —
     the box you typed into glows cyan/violet/pink, and this is the answer
     to what you typed. Two backgrounds with different clips is what draws
     it: the fill is painted to the padding box and the gradient to the
     border box, so the 1px border IS the gradient and nothing has to be
     faked with a pseudo-element behind the card. */
  /* ── THE PLAN CARD ────────────────────────────────────────────────────
     A proposal you read in four seconds and answer with one click.

     It used to be a 560px card with a gradient border in three hues, a
     coloured glow, a 34px shield badge, a display-face title and two
     uppercase kickers. All of that was announcing itself before a word of
     it had been read — and what it announces is the least interesting
     thing on screen, because you already know you asked for a plan. The
     content is the title, the list and the two buttons; everything else
     was frame. */
  .ag-plan{
    position:relative;margin:10px 0 14px;padding:14px 16px 13px;
    border:1px solid var(--line);border-radius:14px;background:var(--card);
    box-shadow:0 1px 2px rgba(0,0,0,.03);
    max-width:480px;animation:agIn .3s var(--ease) forwards}
  .ag-plan>*{position:relative;z-index:1}
  .ag-plan.done{opacity:.6;box-shadow:none}

  .pl-head{margin-bottom:8px}
  .pl-kicker{display:block;font-size:.7rem;font-weight:600;color:var(--mut);margin-bottom:2px}
  .pl-title{display:block;font-size:1rem;font-weight:650;letter-spacing:-.01em;color:var(--ink);line-height:1.3}
  .pl-sum{margin:0 0 12px;font-size:.85rem;line-height:1.5;color:var(--ink-2)}

  .pl-sec{margin:0 0 12px}
  .pl-sec-h,.pl-assume-h{display:block;font-size:.72rem;font-weight:600;
    color:var(--mut);margin-bottom:6px}
  .pl-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}
  .pl-list li{display:flex;align-items:flex-start;gap:8px;font-size:.84rem;color:var(--ink);line-height:1.45}
  .pl-tick{flex:none;width:14px;height:14px;margin-top:2px;color:var(--accent)}
  .pl-tick svg{width:100%;height:100%;display:block}

  /* Assumptions are the part worth arguing with, so they stay visually
     apart — but as a quieter block, not a second card inside the first. */
  .pl-assume{margin:0 0 12px;padding-left:11px;border-left:2px solid var(--line)}
  .pl-assume-h{margin-bottom:4px}
  .pl-assume ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:3px}
  .pl-assume li{font-size:.79rem;color:var(--mut);line-height:1.45}

  .pl-actions{display:flex;flex-wrap:wrap;gap:7px;align-items:center}
  .pl-go,.pl-edit{border:0;border-radius:9px;padding:7px 13px;font:inherit;font-size:.82rem;
    font-weight:600;cursor:pointer;
    transition:background .15s var(--ease),color .15s var(--ease),border-color .15s var(--ease)}
  .pl-go{display:inline-flex;align-items:center;gap:6px;color:#fff;background:var(--accent)}
  .pl-go svg{width:13px;height:13px;transition:transform .15s var(--ease)}
  .pl-go:hover{background:color-mix(in srgb,var(--accent) 88%,#000)}
  .pl-go:hover svg{transform:translateX(2px)}
  .pl-edit{background:transparent;color:var(--ink-2);border:1px solid var(--line)}
  .pl-edit:hover{color:var(--ink);border-color:var(--mut);background:var(--paper-2)}
  .pl-chose{display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:600;color:var(--mut)}
  .pl-chose::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--accent);opacity:.7}

  /* ── A QUESTION WITH ANSWERS YOU CAN PRESS ────────────────────────────
     The agent could already ask; the only way to answer was to type the
     reply yourself, which is slow and invites a wording the assessor then
     has to re-interpret. These are the same options it would have
     described in prose, as buttons — so the common answer is one click
     and the unusual one is still typed. */
  .ag-ask{margin:10px 0 14px;max-width:480px;animation:agIn .3s var(--ease) forwards}
  .ag-ask-q{font-size:.9rem;line-height:1.5;color:var(--ink);margin:0 0 10px}
  .ag-ask-opts{display:flex;flex-direction:column;gap:6px}
  .ag-ask-opt{display:flex;align-items:flex-start;gap:9px;width:100%;text-align:left;
    padding:9px 11px;border-radius:10px;border:1px solid var(--line);background:var(--card);
    font:inherit;font-size:.84rem;color:var(--ink);cursor:pointer;
    transition:border-color .14s var(--ease),background .14s var(--ease),transform .12s var(--ease)}
  .ag-ask-opt:hover{border-color:var(--accent);background:var(--paper-2)}
  .ag-ask-opt:active{transform:translateY(1px)}
  .ag-ask-opt .k{flex:none;width:17px;height:17px;border-radius:5px;margin-top:1px;
    border:1px solid var(--line);display:grid;place-items:center;
    font-size:.62rem;font-weight:700;color:var(--mut);background:var(--paper-2)}
  .ag-ask-opt .t{min-width:0;display:flex;flex-direction:column;gap:1px}
  .ag-ask-opt .t b{font-weight:600;line-height:1.35}
  .ag-ask-opt .t small{font-size:.76rem;color:var(--mut);line-height:1.4}
  /* Recommended is a hint, not a default that presses itself. */
  .ag-ask-opt .rec{margin-inline-start:auto;flex:none;align-self:center;
    font-size:.68rem;font-weight:600;color:var(--accent);
    background:color-mix(in srgb,var(--accent) 11%,transparent);
    border-radius:5px;padding:2px 6px;white-space:nowrap}
  .ag-ask-opt.on{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 7%,transparent)}
  .ag-ask-opt.on .k{background:var(--accent);border-color:var(--accent);color:#fff}
  .ag-ask-foot{display:flex;gap:7px;align-items:center;margin-top:9px}
  .ag-ask-send{border:0;border-radius:9px;padding:7px 13px;font:inherit;font-size:.82rem;
    font-weight:600;color:#fff;background:var(--accent);cursor:pointer;
    transition:background .15s var(--ease),opacity .15s}
  .ag-ask-send[disabled]{opacity:.45;cursor:default}
  .ag-ask-send:not([disabled]):hover{background:color-mix(in srgb,var(--accent) 88%,#000)}
  .ag-ask-hint{font-size:.76rem;color:var(--mut)}
  .ag-ask.done{opacity:.6;pointer-events:none}

  @media (prefers-reduced-motion:reduce){
    .pl-go:hover svg{transform:none}
    .ag-ask-opt:active{transform:none}
  }
  @media (max-width:640px){
    .ag-plan{padding:13px 13px 12px;border-radius:13px}
    .pl-go,.pl-edit{flex:1 1 auto;justify-content:center;text-align:center}
  }

  .ag-set-overlay{position:fixed;inset:0;z-index:9999999 !important;background:var(--scrim);
    display:flex;align-items:center;justify-content:center;padding:clamp(8px,3.5vh,40px);
    -webkit-backdrop-filter:var(--scrim-blur);backdrop-filter:var(--scrim-blur)}
  .ag-set-overlay[hidden]{display:none !important;pointer-events:none !important;visibility:hidden !important}
  /* The settings panel is an iframe, and an iframe appears the instant it
     is displayed — so the box snapped into place and only THEN did the
     headings inside it slide up. The container is what should arrive; the
     text is inside it and comes along.

     The animation restarts on every open by itself: the overlay is
     toggled with [hidden], and going from display:none back to display
     re-runs a CSS animation. */
  .ag-set-overlay iframe{width:min(1393px,92vw);height:min(700px,80vh);border:0;border-radius:13px;
    background:var(--card);box-shadow:0 30px 90px -28px rgba(0,0,0,.55);
    animation:sqSettingsIn var(--dur-chill,.3s) var(--ease-snappy,ease-out) both}
  @keyframes sqSettingsIn{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}
  /* And the same on the way out. [hidden] alone cut the panel dead in one
     frame, which read as a crash next to the way it opens. .closing runs
     the reverse and the overlay is hidden only once it has finished. The
     exit uses the shorter token on purpose: a dismissal that lingers as
     long as the arrival reads as lag. */
  .ag-set-overlay.closing iframe{animation:sqSettingsOut var(--dur-snappy,.12s) var(--ease-snappy,ease-out) both}
  @keyframes sqSettingsOut{from{opacity:1;transform:none}to{opacity:0;transform:translateY(6px) scale(.99)}}
  @media (prefers-reduced-motion:reduce){
    .ag-set-overlay iframe{animation-duration:1ms;animation-name:none;opacity:1}
    .ag-set-overlay.closing iframe{animation-name:none;opacity:0}
  }
  /* (hover:none) and (pointer:coarse) is in here because an iPad is not a
     narrow screen. In landscape it reports 1180 or 1366 CSS pixels — wider
     than most laptops — so a width-only rule handed it the 1393px desktop
     dialog with its 237px sidebar, on a device that has no mouse to hover
     the thing with. The pointer is what actually distinguishes the two. */
  @media (max-width:900px), (hover:none) and (pointer:coarse){
    /* A SHEET, not a page. Full-bleed and square-cornered, Settings stopped
       reading as something opened over the builder and started reading as
       having navigated away from it — with no edge, no scrim and nothing to
       tap beside it to get back. It is a centred card here, the shape the
       same screen has on a phone and a tablet everywhere else: as wide as
       it can be up to 520px, as tall as it can be up to 640, and on a
       screen smaller than that it simply fills what there is. */
    .ag-set-overlay{padding:0}
    .ag-set-overlay iframe{
      /* One size for both, and small: 420x560 is under half of an iPad's
         screen and still the whole of a phone's width, so the sheet reads
         the same on either — a panel over the app, not the app replaced.
         The vw/dvh terms only bite on a screen smaller than the sheet. */
      width:min(420px,92vw);height:min(560px,88dvh);
      border-radius:22px;box-shadow:0 30px 80px -20px rgba(0,0,0,.55)}
  }
  @media (max-width: 650px) {
    .ag-tiles-track {
      max-width: 100% !important;
      justify-content: flex-start !important;
      overflow-x: auto !important;
      scroll-snap-type: x mandatory !important;
      -webkit-overflow-scrolling: touch !important;
      touch-action: pan-x !important;
      padding: 12px 14px 6px !important;
    }
    .ag-tile {
      flex: 0 0 auto !important;
      scroll-snap-align: start !important;
    }
  }
  @media (max-width:900px){
    .ag-history.on{width:240px !important;max-width:80vw !important;height:100dvh !important}
    .ag-history.on .ag-hist-head{flex:0 0 42px !important}
    .ag-history.on .ag-hist-user{padding-top:3px;padding-bottom:5px}
    .ag-history.on .ag-hist-install,.ag-history.on .ag-hist-links{white-space:nowrap;gap:3px}
    .ag-hist-backdrop{left:min(240px,80vw) !important}
    .ag-history.on .ag-hist-search{order:2;margin-left:auto;margin-right:0}
    .ag-history.on .ag-hist-mark{order:3;margin-left:0}
    #agDeskSidebarToggle{left:7px !important}
    #agDeskSidebarToggle.on{left:7px !important}
    #agDeskSidebarToggle:not(.on){display:none !important}
    .ag-history:not(.on){padding-top:0 !important}
    .ag-history.on{overflow:hidden !important;overflow-y:hidden !important}
    .ag-history.on .ag-hist-head{padding-top:6px !important;padding-bottom:4px !important}
    .ag-history.on .ag-ws{margin-top:4px;margin-bottom:4px}
    .ag-history.on .ag-hist-tabs{gap:0;padding-top:0;padding-bottom:2px}
    .ag-history.on .ag-hist-tab{padding-top:5px;padding-bottom:5px}
    .ag-history.on .ag-hist-chats{flex:1 1 0;min-height:0}
    .ag-history.on .ag-chats-list{min-height:0;overflow-y:auto}
    .ag-history.on .ag-chat-item{padding-top:4px;padding-bottom:4px}
    .ag-history.on .ag-hist-foot{padding:4px 8px}
    .ag-history.on .ag-hist-foot .ttl{font-size:.74rem;margin-bottom:2px}
    .ag-history.on .ag-upsell{margin:3px 0 4px;padding:8px}
    .ag-history.on .ag-hist-install,.ag-history.on .ag-hist-links{font-size:.58rem;margin-top:3px}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-user-gear{display:grid !important;flex:none;width:30px;height:24px}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-user-gear{order:1}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-user-main{order:2}
    .ag-history:not(.on):not(:hover):not(.hover-open) .ag-hist-user{padding-bottom:8px}
    .ag-history:not(.on) .ag-user-gear{order:1 !important}
    .ag-history:not(.on) .ag-user-main{order:2 !important}
    .ag-history:not(.on) .ag-hist-user{padding-bottom:8px}
  }

  /* ---- recent-projects flyout -----------------------------------------
     Hovering Projects in the rail lists the recent ones next to it, so you
     can reach an app without loading the page that lists them. Generated
     into all three pages that carry the rail; edit the source, not a copy.

     position:fixed and a direct child of <body>: the rail is a flex column
     with its own stacking and overflow rules on three pages, and an
     absolutely-positioned panel inside it was going to be clipped by one of
     them. Fixed to the viewport sidesteps all of it. */
  /* --fly-room is the space left of the viewport edge, written by place()
     each frame; the fallback keeps this sane before the first one. Without
     it a 302px panel beside an expanded 238px rail needs 548px, and in a
     narrow window it simply ran off the screen. */
  .ag-fly{position:fixed;z-index:120;
    width:max-content;
    min-width:min(252px, var(--fly-room, 252px));
    max-width:min(302px, var(--fly-room, 302px));
    background:var(--card);border:1px solid var(--line);border-radius:15px;
    box-shadow:0 4px 10px -4px rgba(24,20,14,.10),0 22px 48px -20px rgba(24,20,14,.34);
    padding:9px 8px 11px;opacity:0;visibility:hidden;transform:translateX(-6px);pointer-events:none;
    transition:opacity .16s ease,transform .16s var(--ease,ease),visibility .16s}
  :root[data-theme="dark"] .ag-fly{box-shadow:0 4px 10px -4px rgba(0,0,0,.5),0 24px 52px -20px rgba(0,0,0,.85)}
  .ag-fly.open{opacity:1;visibility:visible;transform:none;pointer-events:auto}
  /* No hover bridge: the panel is flush against the rail, so there is no gap
     for the pointer to cross. The 14px strip that used to span it would now
     lie ON the rail and swallow clicks meant for the tabs underneath. */
  /* Names, at a size you read rather than scan. The coloured tile that was
     here repeated the one on the card the link leads to and made a list of
     six titles look like a toolbar. */
  .ag-fly .ttl{font-size:.79rem;font-weight:450;color:var(--mut);padding:4px 12px 9px}
  /* The list scrolls, not the panel: the hover bridge is pinned to the panel
     and would scroll out from under the pointer along with everything else. */
  .ag-fly .lst{max-height:min(58vh,392px);overflow-y:auto;overscroll-behavior:contain;
    scrollbar-width:thin}
  .ag-fly a{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:10px;
    color:var(--ink);text-decoration:none;font-size:.9rem;line-height:1.3}
  .ag-fly a:hover{background:var(--paper-2)}
  /* Where you already are. Without it the list gives no clue which of these
     is the page you are looking at. */
  .ag-fly a.on{background:var(--paper-2);font-weight:600}
  .ag-fly a .tx{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* Only rendered when two rows would otherwise read identically - building
     the same thing twice gave two rows both saying "build e commerce for
     electronics" and no way to tell which was which. */
  .ag-fly a .wh{flex:none;font-size:.76rem;color:var(--mut);font-variant-numeric:tabular-nums}
  /* The one mark left: it says the app is live, which the name cannot. */
  .ag-fly a .dt{width:6px;height:6px;border-radius:50%;background:var(--ok);flex:none}
  .ag-fly .none{padding:6px 12px 8px;font-size:.85rem;color:var(--mut)}
  /* Loading is the rail's own skeleton at this panel's scale - the same
     sweep, rows the height of the names about to replace them. One short
     "Loading" line is a different shape from the list, so the panel jumped
     to three times its height under the pointer as the names landed. */
  .ag-fly .sk-row{padding:12px 12px;border-radius:10px}
  .ag-fly .sk-row b{height:10px}
  .ag-fly .start{color:var(--accent)}
  /* Touch has no hover: the panel would open on the tap that navigates and
     then sit there over the page it opened. */
  /* Touch only. The width half of this used to be `,(max-width:900px)`,
     matching the breakpoint where the rail becomes a drawer — defensible,
     but it meant a 767px desktop window never showed the panel at all, and
     that is a real window someone works in. Below that breakpoint the rail
     is a drawer you open, and while it is open the tab is there to hover
     like any other, so there is nothing to hide from. Touch stays excluded
     because there is no cursor to hover with: the panel would open on the
     tap that navigates and then sit over the page it opened. */
  @media (hover:none){ .ag-fly{display:none !important} }

  /* ---- last resort, and it has to be last -----------------------------
     Below this the rail is shorter than its own contents even with the
     history at nothing, and everything still in it is load-bearing. So it
     scrolls rather than trimming further: on a landscape phone the account
     row was being pushed past the bottom edge with no way to reach it —
     the same silent loss as the clipped plan card, one level up. The
     account row stays pinned while the rest of it moves.

     Position matters. The max-width:900px block above also sets overflow
     on .ag-history.on with !important and the same specificity, so this
     only wins by coming after it; written up beside the other height rules
     it lost, and lost the way CSS does — the rail simply stayed unscrollable
     with no error anywhere. Selectors are prefixed .ag-history.on for the
     same reason: the mobile block reaches these children that way too.

     The hover flyout is switched off here: it has to paint outside the rail
     and overflow-y:auto would clip it, and a list of recent projects was
     never going to fit in 560px of window. */
  @media (max-height:559px){
    .ag-history.on{overflow-y:auto !important;overflow-x:hidden !important}
    .ag-history.on .ag-hist-chats{min-height:96px}
    .ag-history.on .ag-hist-user{position:sticky;bottom:0;z-index:2}
    .ag-fly{display:none !important}
  }

  /* The plan card arrives rather than appearing. Same numbers as the rest
     of the product; see the fade-up block above. */
  .ag-hist-foot{animation:sqFadeUp var(--dur-chill) var(--ease-snappy) both}
  @media (prefers-reduced-motion:reduce){
    .ag-hist-foot{animation-duration:1ms;--fade-up-shift:0px}
  }

  /* How much room is left in the brief.

     Hidden until it matters. A counter sitting at "12 / 16,000" from the
     first keystroke is nagging about a limit nobody is near; this appears
     at 80% and turns red at the edge, so the only person who ever sees it
     is the person it is about to affect. Which is the whole point — the
     old behaviour let you write a long, careful brief and told you it was
     too long only after you pressed send. */
  .ag-count{position:absolute;right:12px;bottom:-18px;font-size:.7rem;color:var(--mut);
    font-variant-numeric:tabular-nums;opacity:0;transition:opacity var(--dur-snappy,.12s) ease;pointer-events:none}
  .ag-count.show{opacity:1}
  .ag-count.over{color:var(--danger);font-weight:600}

  /* ---- usage, the way Claude reports it ---------------------------------
     Two limits, both shown, because both can stop a build and they stop it
     for different lengths of time. The session bar is the one that matters
     minute to minute — it refills — so it is first and it carries the
     countdown. The month sits under it, quieter, because "wait three weeks"
     is not a thing anyone can act on in the moment.

     A percentage AND a bar: the bar is read at a glance, the number is what
     you check when the bar looks close. Colour alone never carries it —
     amber and red are reinforced by the number moving toward 100%. */
  /* The glow is a TINT, not a shadow - a panel filled with a wash of the
     accent, which is what the reference shows. Two shadow attempts missed
     that: an outer halo lit up the rows either side of the card, and an
     inner one lit the card's edges while its middle stayed the same grey
     as the rest of the rail.

     At rest the card is plain --card. The wash is the HOVER state and only
     the hover state; painted on permanently it stops being a response to
     anything and just makes the rail two-tone. */
  .ag-usage{display:flex;flex-direction:column;gap:9px;padding:10px 11px;border-radius:12px;
    border:1px solid var(--line);background:var(--card);box-shadow:var(--sh-subtle);
    transition:background var(--dur-snappy,.12s) ease,border-color var(--dur-snappy,.12s) ease,
               box-shadow var(--dur-snappy,.12s) ease}
  /* Replit lights a card up rather than merely lifting it: the border warms
     toward the accent and the shadow picks up the accent's colour, so the
     surface reads as live under the pointer instead of just raised. Both
     halves are color-mix on --accent, which keeps it correct in either
     theme - a fixed blue glow is invisible on a light card and garish on a
     dark one. The neutral --sh-subtle stays underneath so the card does not
     lose its own weight while the glow sits on top of it. */
  /* Hover fills the card with the wash, at the strength that actually
     reads: #daeff8 on a #f6f5f4 rail in light, #243b46 on #18191b in
     dark. One inset layer gathers the light toward the edges. Nothing
     leaves the box - an outer halo in a 238px rail lands on the Recent
     list above it and the install row below. */
  .ag-usage:hover{border-color:color-mix(in srgb,var(--accent) 30%,var(--line));
    background:color-mix(in srgb,var(--accent) 15%,var(--card));
    box-shadow:var(--sh-subtle),
      inset 0 0 20px -6px color-mix(in srgb,var(--accent) 34%,transparent)}
  @media (prefers-reduced-motion:reduce){ .ag-usage{transition:none} }
  .ag-usage .u-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
  .ag-usage .u-plan{font-size:.78rem;font-weight:600;color:var(--ink);text-transform:capitalize}
  /* Mixed toward --ink rather than the bare --accent. On the tinted card
     the raw accent came to 4.23:1 in dark and 2.34:1 in light, both under
     AA for text this size - the light figure was already failing before
     the tint, the dark one is the tint's doing. Mixing deepens it on the
     pale card and lifts it on the dark one, from one declaration. */
  .ag-usage .u-up{font-size:.72rem;font-weight:600;text-decoration:none;
    color:color-mix(in srgb,var(--accent) 58%,var(--ink));
    display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
  .ag-usage .u-up:hover{text-decoration:underline}
  .ag-usage .u-up svg{width:12px;height:12px}
  .u-row{display:flex;flex-direction:column;gap:4px}
  .u-lab{display:flex;align-items:baseline;justify-content:space-between;gap:8px;
    font-size:.71rem;color:var(--ink-2)}
  .u-lab b{font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
  .u-bar{height:5px;border-radius:99px;background:color-mix(in srgb,currentColor 12%,transparent);
    overflow:hidden;color:var(--ink-2)}
  .u-bar i{display:block;height:100%;border-radius:99px;background:var(--accent);
    width:0;transition:width var(--dur-chill,.3s) var(--ease-snappy,ease-out)}
  .u-bar i.warn{background:var(--warn)}
  .u-bar i.hot{background:var(--danger)}
  .u-when{font-size:.67rem;color:var(--mut);font-variant-numeric:tabular-nums}
  .u-row.quiet .u-lab,.u-row.quiet .u-when{color:var(--mut)}
  @media (prefers-reduced-motion:reduce){ .u-bar i{transition:none} }

  /* ---- what to do next --------------------------------------------------
     Offered as buttons rather than described in the reply, because the
     answer to "shall I add a dark mode toggle?" is one tap, and making
     someone retype the idea to accept it is a strange thing to ask.

     Each chip IS the yes. There is no separate Yes button per row, because
     a Yes that needs its own target doubles the number of things to aim at
     for no gain — tapping the idea you want is the least ambiguous accept
     there is. The single Dismiss is the no, and it is quiet: declining is
     the common case and should not look like the important one. */
  .ag-next{display:flex;flex-direction:column;gap:7px;margin:10px 0 2px;
    animation:sqFadeUp var(--dur-chill,.3s) var(--ease-snappy,ease-out) both}
  .ag-next .n-ttl{font-size:.73rem;color:var(--mut);font-weight:500}
  .ag-next .n-row{display:flex;flex-wrap:wrap;gap:7px;align-items:center}
  .ag-next button.n-yes{display:inline-flex;align-items:center;gap:7px;
    border:1px solid var(--line);background:var(--card);color:var(--ink);
    border-radius:999px;padding:7px 13px 7px 11px;font:inherit;font-size:.79rem;font-weight:500;
    cursor:pointer;text-align:left;box-shadow:var(--sh-subtle);
    transition:border-color var(--dur-snappy,.12s) ease,background var(--dur-snappy,.12s) ease,
               transform var(--dur-snappy,.12s) ease}
  .ag-next button.n-yes:hover{border-color:var(--line-hover);background:var(--paper-2);transform:translateY(-1px)}
  .ag-next button.n-yes:active{transform:none}
  .ag-next button.n-yes svg{width:13px;height:13px;flex:none;color:var(--accent)}
  .ag-next button.n-no{border:0;background:none;color:var(--mut);font:inherit;font-size:.75rem;
    cursor:pointer;padding:7px 4px;text-decoration:underline;text-underline-offset:2px}
  .ag-next button.n-no:hover{color:var(--ink-2)}
  .ag-next.busy{opacity:.5;pointer-events:none}
  @media (prefers-reduced-motion:reduce){ .ag-next{animation-duration:1ms} }

  /* ---- once there is a conversation, the new-chat screen is gone --------
     agNewExtras (the build-type picker and example prompts) and agRecent
     were hidden by JS, but only on the branch that runs after a FIRST
     build. Reopening an existing project takes a different path, so on
     /agent/<slug> they were never hidden at all — 489px of new-chat
     furniture still sitting below the composer.

     On a phone that is the whole bug people see: .ag is a flex column, so
     that furniture takes its space first and the log is left with what
     remains, which measured 20 pixels. The composer ends up near the top
     of the screen with a field of nothing under it, and the chat it
     belongs to is a 20px slot above it.

     Stated in CSS keyed on .is-new rather than fixed in the one JS branch,
     because the state is already on the element and a rule cannot be
     reached down a code path nobody remembered. */
  /* Keyed on "there is a conversation", not on "is-new was cleared".

     .is-new is supposed to come off when a turn starts, and mostly it
     does — but a page that still carried it WITH restored history put the
     composer 619px below the bottom of a 608px phone, measured. Relying
     on a flag being cleared by the right code path is how that happens;
     the log's own contents cannot be out of date.

     :not(.ag-empty) is what makes it precise. A fresh log is not empty —
     it holds exactly one child, the greeting — so :empty and :has(> *)
     both answer wrong. Any child that is not the greeting is a real turn. */
  .ag:not(.is-new) #agNewExtras,
  .ag:not(.is-new) #agRecent,
  .ag:has(#agLog > *:not(.ag-empty)) #agNewExtras,
  .ag:has(#agLog > *:not(.ag-empty)) #agRecent{display:none !important}

  /* The hero is a padded flex COLUMN on the new-chat screen, which is what
     lets a long log push the composer off the bottom. Back to
     display:contents once there is a conversation, so the composer is a
     direct flex child of .ag again and stays pinned. */
  .ag.is-new:has(#agLog > *:not(.ag-empty)) .ag-hero{display:contents}

  /* The log takes the room that frees up, and scrolls inside itself, so
     the composer sits on the bottom edge where it belongs. */
  .ag:not(.is-new) #agLog,
  .ag:has(#agLog > *:not(.ag-empty)) #agLog{flex:1 1 auto;min-height:0;overflow-y:auto}

  /* And the SECTION stops scrolling. .ag.is-new sets overflow-y:auto so the
     new-chat screen can scroll as one page; left on with a real conversation
     restored, the whole column scrolls instead of the log, and the composer
     — a child of that column, not a fixed element — slides up the screen
     with it and comes to rest over the messages.

     The rules above already stopped trusting .is-new to be cleared. This is
     the one declaration that was still trusting it. */
  /* .ag.is-new{overflow-y:auto!important} lets the new-chat screen scroll as
     one page. With a conversation restored that turns the whole column into
     the scroller instead of the log, and the composer travels with it. */
  .ag.is-new:has(#agLog > *:not(.ag-empty)),
  .ag.is-new.has-convo{overflow:hidden !important;overflow-y:hidden !important}
  .ag.has-convo .ag-hero{display:contents}
  .ag.has-convo #agLog{flex:1 1 auto;min-height:0;overflow-y:auto}


  /* Laid out the way Claude's own usage popover does it: the limit's name
     on the left, and its reset time and percentage together on the right
     of the SAME line, with the bar under both. Reading "how full" and
     "when does it clear" as one line is the whole point — they are the
     two halves of one question, and splitting the reset onto its own row
     below the bar (which is what this did before) made it read as a
     footnote to the bar rather than as half the answer. */
  .u-lab{display:flex;align-items:baseline;justify-content:space-between;gap:7px;
    font-size:.72rem;color:var(--ink-2);width:100%}
  .u-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:none}
  /* Claude has a 460px popover for this line; the rail has 194px. At the
     worst case it actually has to render - "5-hour limit / Resets in 2h37m
     / 100%" - the three parts came to 2px under the width, and the part
     that gave way was the LABEL, so the row read "5-hour limi...". Wrong
     one: the label is what the row is about, the countdown is the detail.
     So the label is flex:none, and the countdown is what truncates. */
  .u-meta{display:flex;align-items:baseline;gap:7px;flex:0 1 auto;min-width:0}
  /* --ink-2, not --mut. On the untinted card --mut already only managed
     3.58:1 against it; the accent wash took that to 2.90, which is under
     AA for text this size. --ink-2 puts it back to 4.61. The hierarchy
     still holds - the percentage beside it is --ink and bold. */
  .u-reset{color:var(--ink-2);font-size:.67rem;white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis;min-width:0}
  .u-meta b{flex:none}
  .u-lab b{font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}

  @media (max-width:900px){
    /* .ag-split is height:calc(100vh - 56px) — 56px held back for the top
       bar. That bar (.ag-head) is display:none at this width, so the 56px
       was reserved for nothing and showed up as exactly 56px of dead space
       under the composer: measured 552 tall in a 608 viewport, which is
       the gap in the report.

       dvh rather than vh on purpose. On a phone, vh is the LARGEST the
       viewport gets — the value with the browser's own chrome retracted —
       so a composer pinned to 100vh sits under the address bar until you
       scroll. dvh tracks the viewport that is actually on screen. */
    .ag-split{height:100dvh !important}
  }

  /* The rail does NOT animate in.

     The plan card had the same fade-up as the page's cards, and on a rail
     that is present before you look at it — every load, every refresh —
     an entrance is not an arrival, it is a flicker. Chrome that announces
     itself on a page you were already on reads as cheap precisely because
     nothing arrived: the sidebar was always going to be there.

     Kept for content that genuinely appears (the card grid on /projects,
     a settings panel you switched to); removed for furniture. */
  .ag-hist-foot,.ag-usage{animation:none !important}

  @media (max-width:900px){
    /* The account avatar sat 4px left of the workspace one above it.
       Same cause the .ag-ws rule already documents: .nm is still a flex
       item at max-width:0, so the 8px gap beside it is real and the pair
       gets centred as 30px of content rather than 22px of avatar. gap:0
       removes the phantom half.

       Sized to match the pill above too — two circles of different sizes
       stacked in a 44px column read as a mistake even when both are
       centred. */
    .ag-history:not(.on) .ag-user-main{gap:0 !important;justify-content:center}
    .ag-history:not(.on) .ag-user-main .av{width:26px;height:26px;font-size:.82rem}
  }

  @keyframes sweep{to{transform:translateX(100%)}}
  /* ---- the rail's own skeleton -----------------------------------------
     The card grids on /projects and /deployments already shimmer a
     placeholder the shape of what is coming. The rail said the word
     "Loading…" instead, which is the one thing a skeleton exists to avoid:
     a line of text is not the shape of the list, so the panel jumps when
     the real rows land.

     Same sweep, same timing, at row scale — a dot for the status mark and
     a bar for the name, because that is what a chat row is. Widths vary a
     little per row so it reads as a list of different titles rather than
     a stack of identical bars. */
  .sk-row{display:flex;align-items:center;gap:10px;padding:7px 6px;border-radius:8px;
    position:relative;overflow:hidden}
  .sk-row::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
    background:linear-gradient(90deg,transparent,rgba(139,152,165,.13),transparent);
    animation:sweep 1.4s var(--ease) infinite}
  .sk-row i{width:6px;height:6px;border-radius:50%;flex:none;background:var(--line)}
  .sk-row b{height:8px;border-radius:99px;background:var(--line);display:block}
  .sk-row:nth-child(1) b{width:74%}
  .sk-row:nth-child(2) b{width:58%}
  .sk-row:nth-child(3) b{width:66%}
  .sk-row:nth-child(4) b{width:47%}
  .sk-row:nth-child(5) b{width:62%}
  @media (prefers-reduced-motion:reduce){ .sk-row::after{animation:none} }

  /* Phone sizing for the mode panel, matching the home page's copy. The
     builder already clamps this panel horizontally (see the pill handler);
     it was simply still 250px wide, which is most of a 351px screen. */
  @media (max-width:480px){
    .ag-model-popover{width:min(216px,calc(100vw - 24px));padding:6px;border-radius:12px}
    .pop-section-ttl{padding:3px 6px 4px;font-size:.66rem}
    .pop-option{padding:7px 6px;gap:8px}
    .pop-option .pop-text b{font-size:.8rem}
    .pop-option .pop-text small{font-size:.7rem}
  }

