    /* A4 type rhythm */
    .view-title h2,.wh-day{font-size:var(--type-display);font-weight:800;line-height:var(--lh-head);letter-spacing:0}
    .section-head h2,.wk-ttl{font-size:var(--type-heading);font-weight:800;line-height:var(--lh-head);letter-spacing:0}
    .row-title,.tp-name,.ex-row-name,.ex-name,.fi-name,.crew-name,.cc-ttl,.hist-day,.pe-lift-name,.cp-ttl,.chat-ttl,.str-name,.plan-day{font-size:var(--type-item);font-weight:700;line-height:var(--lh-head)}
    .row-sub,.section-head p,.view-title p,.tp-sub,.ex-row-meta,.fi-serv,.fi-macros,.hist-sub,.crew-sub,.cc-sub,.cg-sub,.pe-hint,.plan-hint,.wh-sub,.field-hint,.sched-note,.ex-lib-meta,.pe-lift-meta,.kb-hint,.cw-sets{font-size:var(--type-meta);font-weight:500;line-height:var(--lh-body)}
    .list-label,.schedule-kicker,.wh-kicker,.plan-today,.cp-dow,.macro-l,.cal-meta span,.week-foot span,.ci-cell span,.set-grid-h,.cc-tag,.chat-sub,.wk-dow{font-size:var(--type-label);font-weight:800;line-height:1;letter-spacing:.08em}
    .skel{position:relative;display:block;overflow:hidden;background:var(--surface);border:1px solid var(--faint);border-radius:var(--r-md)}
    .skel::after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,var(--faint),transparent);opacity:.62;animation:skel-shimmer 1.2s linear infinite}
    @keyframes skel-shimmer{to{transform:translateX(100%)}}
    .skel-line{height:12px;border-radius:var(--r-pill);border:0}
    .skel-line.sm{height:10px}
    .skel-w-25{width:25%}.skel-w-35{width:35%}.skel-w-45{width:45%}.skel-w-60{width:60%}.skel-w-75{width:75%}.skel-w-90{width:90%}
    .skel-row{cursor:default;pointer-events:none}
    .skel-avatar{flex:none;width:40px;height:40px;border-radius:var(--r-md)}
    .skel-stack{display:flex;flex:1;min-width:0;flex-direction:column;gap:7px}
    .skel-chip{flex:none;width:54px;height:22px;border-radius:var(--r-pill)}
    .skel-panel{display:flex;flex-direction:column}
    .skel-card{height:76px;margin-top:24px}
    .chat-skel-wrap{display:flex;flex-direction:column;gap:12px;padding:14px 16px}
    .skel-bubble{width:74%;height:42px;border-radius:var(--r-lg)}
    .skel-bubble.mine{align-self:flex-end;width:64%}
    .skel-bubble.wrap{width:92%;height:92px;border-radius:var(--r-lg)}
    .food-result.skel-result{cursor:default;pointer-events:none}
    .food-result.skel-result .skel-line + .skel-line{margin-top:8px}
    @media (prefers-reduced-motion:no-preference){
      ::view-transition-old(root){animation:view-out 190ms ease-out both}
      ::view-transition-new(root){animation:view-in 210ms cubic-bezier(.22,.61,.36,1) both}
      @keyframes view-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-6px)}}
      @keyframes view-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
    }
    /* B2 staggered list entrances. Children animate ONLY when #app carries .stagger-on
       (added by swapView after a real view change, removed after the window), so bare
       re-renders / keystrokes / timer ticks never replay it. Capped at the first 8 children
       (later rows appear instantly). Pure transform+opacity, no layout change. */
    #app.stagger-on .stagger > *{animation:stagger-rise 180ms ease-out both}
    #app.stagger-on .stagger > *:nth-child(1){animation-delay:0ms}
    #app.stagger-on .stagger > *:nth-child(2){animation-delay:22ms}
    #app.stagger-on .stagger > *:nth-child(3){animation-delay:44ms}
    #app.stagger-on .stagger > *:nth-child(4){animation-delay:66ms}
    #app.stagger-on .stagger > *:nth-child(5){animation-delay:88ms}
    #app.stagger-on .stagger > *:nth-child(6){animation-delay:110ms}
    #app.stagger-on .stagger > *:nth-child(7){animation-delay:132ms}
    #app.stagger-on .stagger > *:nth-child(8){animation-delay:154ms}
    #app.stagger-on .stagger > *:nth-child(n+9){animation:none}
    @keyframes stagger-rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
    /* B4 sheet/modal choreography. Enter: backdrop fade 150ms + sheet translateY(12px) with
       opacity 0->1 over 200ms ease-out. Exit (.closing): 150ms reverse; the element is removed
       from the DOM on transitionend with a 250ms setTimeout failsafe (see closeModal). */
    .modal-backdrop.open{animation:mb-in 150ms ease-out both}
    .modal-backdrop.open .sheet{animation:sheet-in 200ms ease-out both}
    .modal-backdrop.closing{display:flex;animation:mb-out 150ms ease-out both}
    .modal-backdrop.closing .sheet{animation:sheet-out 150ms ease-out both}
    @keyframes mb-in{from{opacity:0}to{opacity:1}}
    @keyframes mb-out{from{opacity:1}to{opacity:0}}
    @keyframes sheet-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
    @keyframes sheet-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(12px)}}
    /* Polish Standard (2026-07-05): press states (item 3) + reduced-motion (item 4). Pure transform/opacity, no layout change. */
    .nav-item:active,.crew-row:active,.chat-send:active,.quick-chip:active,.ex-row:active,.secondary-wide:active,.small-button:active,.manual-toggle:active,.danger-wide:active,.nudge-chip:active{transform:scale(.97);opacity:.9}
    /* A5 (2026-07-04 devibe): accessible keyboard/switch focus ring. Tokenized via --cta so both editions inherit; keyboard-only (:focus-visible), never fires on tap/click. */
    button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible,[role="button"]:focus-visible,.nav-item:focus-visible,.crew-row:focus-visible,.chat-send:focus-visible,.quick-chip:focus-visible,.ex-row:focus-visible,.secondary-wide:focus-visible,.small-button:focus-visible,.manual-toggle:focus-visible,.danger-wide:focus-visible,.nudge-chip:focus-visible,.pill-button:focus-visible,.icon-button:focus-visible{outline:2px solid var(--cta);outline-offset:2px;border-radius:inherit}
    /* B3 micro-interactions (2026-07-06). One-shot, transform/opacity only, no layout shift.
       Each is self-limiting so the re-render-on-every-state-change architecture never strands
       or replays it: .set-tick and .pr-pop are added at their DOM sites and removed on
       animationend; karma-float rides the existing #app.stagger-on view-entry window (no new
       observer); chat-settle keys off the transient .chat-msg.pending state (present only while
       an optimistic echo is in flight). Toast slide+fade already ships via the .toast transition. */
    .set-grid.set-tick{animation:set-tick 200ms ease-out both}
    @keyframes set-tick{0%{transform:scale(1)}45%{transform:scale(1.04)}100%{transform:scale(1)}}
    .pr-pop{display:inline-block;animation:pr-pop 250ms cubic-bezier(.22,.61,.36,1) both}
    @keyframes pr-pop{0%{transform:scale(1)}55%{transform:scale(1.12)}100%{transform:scale(1)}}
    #app.stagger-on .stagger .cc-karma{animation:karma-float 200ms ease-out both;animation-delay:120ms}
    @keyframes karma-float{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
    .chat-msg.pending .chat-bubble,.chat-msg.pending .chat-gifwrap{animation:chat-settle 150ms ease-out both}
    @keyframes chat-settle{from{opacity:.4;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
    /* ===== Fun Pack 1 (2026-07-06): reward-moment animations. Transforms/opacity only,
       one-shot, latched against the re-render-on-every-state-change architecture (B3 pattern),
       every one respects the reduced-motion block below. House-kit spring curves generated with
       Agents/_shared/tools/spring-curve.mjs. ===== */
    /* 1. PR celebration: spring slam (scale 1.8 -> 1 overshoot + rotation settle) + particle burst.
       Curve: spring-curve.mjs --bounce 0.55 --duration 450 (settles 464ms). */
    .pr-pop.pr-slam{animation:pr-slam 464ms linear(0,0.0163,0.0609,0.1276,0.2104,0.304,0.4035,0.5048,0.6044,0.6994,0.7876,0.8673,0.9376,0.9977,1.0476,1.0875,1.1177,1.139,1.1522,1.1584,1.1446,1.1185,1.103,1.087,1.0709,1.0554,1.0408,1.0274,1.005,0.9891,0.9767,0.9899,1.0022,1) both}
    @keyframes pr-slam{0%{transform:scale(1.8) rotate(-8deg)}100%{transform:scale(1) rotate(0)}}
    .pr-burst{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;z-index:3}
    .pr-fleck{position:absolute;left:0;top:0;width:7px;height:7px;border-radius:2px;opacity:0;will-change:transform,opacity;animation:pr-fleck 600ms ease-out both}
    @keyframes pr-fleck{0%{opacity:1;transform:translate(0,0) scale(.4)}20%{opacity:1}100%{opacity:0;transform:translate(var(--fx),var(--fy)) scale(1)}}
    /* 2. Karma leaderboard FLIP: rows glide from their old rect to the new one.
       Curve: spring-curve.mjs --bounce 0.28 --duration 350 (settles 424ms). */
    .kb-row.flip-move{animation:kb-flip 424ms linear(0,0.0121,0.0445,0.0922,0.1507,0.2164,0.2862,0.3575,0.4284,0.4974,0.5631,0.6249,0.6821,0.7344,0.7817,0.8238,0.8609,0.8933,0.9212,0.945,0.9649,0.9813,0.9946,1.0134,1.0267,1.0131,1.0006,1) both}
    @keyframes kb-flip{from{transform:translateY(var(--flip-dy))}to{transform:translateY(0)}}
    /* crown hop when the leader changes. Curve: --bounce 0.45 --duration 380 (settles 456ms). */
    .k-crown.crown-hop{display:inline-block;animation:crown-hop 456ms linear(0,0.0163,0.0603,0.1249,0.2042,0.2926,0.3856,0.4793,0.5706,0.6571,0.7371,0.8094,0.8732,0.9282,0.9745,1.0123,1.0422,1.0647,1.0808,1.0964,1.0844,1.0677,1.0495,1.0322,1.0176,1.0019,1) both}
    @keyframes crown-hop{from{transform:translateY(-8px)}to{transform:translateY(0)}}
    /* 4. Reaction fly-up: a floating copy of the tapped emoji rises + drifts + fades. */
    .react-flyup{position:fixed;z-index:60;pointer-events:none;font-size:22px;line-height:1;will-change:transform,opacity;animation:react-flyup 700ms ease-out both}
    @keyframes react-flyup{0%{opacity:1;transform:translate(-50%,-50%) scale(.9)}15%{opacity:1}100%{opacity:0;transform:translate(calc(-50% + var(--rx)),calc(-50% - 60px)) scale(1.1)}}
    /* 5. Set-complete checkmark draw: the SVG check strokes itself in when marked done. */
    .sg-chk .sg-check{width:20px;height:20px;display:block;margin:auto}
    .sg-chk .sg-check path{stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;fill:none;stroke-dasharray:24;stroke-dashoffset:24}
    .sg-chk.on .sg-check path{stroke-dashoffset:0}
    .sg-chk.draw .sg-check path{animation:sg-draw 250ms ease-out both}
    @keyframes sg-draw{from{stroke-dashoffset:24}to{stroke-dashoffset:0}}
    /* 6. Streak wiggle: one small spring wiggle when the streak value increases. */
    .streak-chip.wiggle{animation:streak-wiggle 400ms ease-in-out both;transform-origin:center}
    @keyframes streak-wiggle{0%{transform:rotate(0)}25%{transform:rotate(-6deg)}60%{transform:rotate(4deg)}100%{transform:rotate(0)}}
    @media (prefers-reduced-motion:reduce){
      *{animation:none!important;transition:none!important}
      ::view-transition-old(root),::view-transition-new(root){animation:none!important}
      .skel::after{display:none}
      /* stagger children + sheet render static/visible (never stranded at opacity:0). */
      #app.stagger-on .stagger > *{opacity:1;transform:none}
      .modal-backdrop.open .sheet,.modal-backdrop.closing .sheet{opacity:1;transform:none}
      /* B3 micro-interactions render static (never stranded at opacity:0 or scale!=1). */
      .set-grid.set-tick,.pr-pop{transform:none}
      #app.stagger-on .stagger .cc-karma{opacity:1;transform:none}
      .chat-msg.pending .chat-bubble,.chat-msg.pending .chat-gifwrap{opacity:1;transform:none}
      .nav-item:active,.crew-row:active,.chat-send:active,.quick-chip:active,.ex-row:active,.secondary-wide:active,.small-button:active,.manual-toggle:active,.danger-wide:active,.nudge-chip:active{transform:none;opacity:.85}
      /* Fun Pack 1 render static: PR slam/crown/FLIP/wiggle land on their final transform; the
         checkmark shows fully drawn when done; JS-spawned flecks/flyups are one-shot decorations
         (JS skips spawning them entirely under reduced-motion, so nothing is stranded). */
      .pr-pop.pr-slam,.kb-row.flip-move,.k-crown.crown-hop,.streak-chip.wiggle{transform:none}
      .sg-chk.on .sg-check path,.sg-chk.draw .sg-check path{stroke-dashoffset:0}
    }

