/**
 * live-apps.jsx — /app/live-apps surface.
 *
 * Current layout: a 3-column container of 6 tab cells (2 rows of 3),
 * styled as one continuous panel with a 2px orange top accent and thin
 * dark dividers between cells (per the reference screenshot from
 * 2026-06-05). Clicking a cell opens a single shared content section
 * below the tab grid with a smooth slide-down. Clicking the active
 * cell closes the panel; clicking a different cell keeps the panel
 * open and swaps its content.
 *
 * The previous AVNU / VESU / ENDUR / TROVES app grid + right-slide
 * detail panel lives at the bottom of this file under `LEGACY APPS
 * GRID` — kept commented out so it can be plugged back in when we're
 * ready to surface the live ecosystem cards again. Original code is
 * preserved in git history at commit efeae7d on feat/live-apps.
 */
/* No-bundler form: React is loaded via unpkg as a window global, and
 * shielded-mode.jsx is loaded as its own <script type="text/babel">
 * BEFORE this file so ShieldedModeModule is reachable here. */
const { useState, useEffect, useRef } = React;
// ShieldedModeModule is a window global — declared in shielded-mode.jsx.

/* Inline SVGs for the mobile tab strip — Lucide-style outline icons
 * (24x24 grid, 1.75 stroke, round caps + joins). currentColor lets the
 * active / filled state tint them with the same color as surrounding
 * text. Paths picked for instant recognition at 22px on a phone. */
const TAB_ICON_SVG_PROPS = {
  viewBox: '0 0 24 24', width: 22, height: 22,
  fill: 'none', stroke: 'currentColor', strokeWidth: 1.75,
  strokeLinecap: 'round', strokeLinejoin: 'round',
  'aria-hidden': 'true',
}
const TabIcon = {
  /* Wallet — Lucide "wallet". Classic pocket with the corner card slot. */
  wallet: (
    <svg {...TAB_ICON_SVG_PROPS}>
      <path d="M21 12V7H5a2 2 0 0 1 0-4h14v4" />
      <path d="M3 5v14a2 2 0 0 0 2 2h16v-5" />
      <path d="M18 12a2 2 0 0 0 0 4h4v-4Z" />
    </svg>
  ),
  /* Bridge — Lucide "arrow-right-left". Two opposing arrows. Reads as
     a cross-chain bridge / swap, which is exactly what this tab is for. */
  bridge: (
    <svg {...TAB_ICON_SVG_PROPS}>
      <path d="m16 3 4 4-4 4" />
      <path d="M20 7H4" />
      <path d="m8 21-4-4 4-4" />
      <path d="M4 17h16" />
    </svg>
  ),
  /* Apps / Private DeFi — Lucide "layout-grid". 2x2 grid of rounded
     tiles. Reads instantly as "apps". */
  apps: (
    <svg {...TAB_ICON_SVG_PROPS}>
      <rect width="7" height="7" x="3" y="3" rx="1" />
      <rect width="7" height="7" x="14" y="3" rx="1" />
      <rect width="7" height="7" x="14" y="14" rx="1" />
      <rect width="7" height="7" x="3" y="14" rx="1" />
    </svg>
  ),
  /* Shield Funds — Lucide "shield-check". Cleaner curves than the
     previous hand-drawn version; the checkmark sits inside the shield
     so it reads at small sizes. */
  shield: (
    <svg {...TAB_ICON_SVG_PROPS}>
      <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
      <path d="m9 12 2 2 4-4" />
    </svg>
  ),
  /* Shield PFP — Lucide "user-round". Slightly bigger head, clean
     shoulder arc. */
  user: (
    <svg {...TAB_ICON_SVG_PROPS}>
      <circle cx="12" cy="8" r="5" />
      <path d="M20 21a8 8 0 0 0-16 0" />
    </svg>
  ),
}

const TABS = [
  { id: 0, number: '01', title: 'Get Wallet',   mobileTitle: 'Wallet', icon: TabIcon.wallet },
  { id: 1, number: '02', title: 'Bridge',       mobileTitle: 'Bridge', icon: TabIcon.bridge },
  // Shield tab (id 2) temporarily removed; ShieldModule + side
  // panels stay defined below so reinstating it is a one-line add.
  { id: 3, number: '03', title: 'Private Defi', mobileTitle: 'DeFi',   icon: TabIcon.apps },
  { id: 4, number: '04', title: 'Shield Funds', mobileTitle: 'Funds',  icon: TabIcon.shield },
  { id: 5, number: '05', title: 'Shield PFP',   mobileTitle: 'PFP',    icon: TabIcon.user },
]

/* ── Wallets shown inside the Get Wallet (tab 01) panel.
 *    extensions   → desktop browser extensions (chrome / firefox / etc.)
 *    mobile.ios   → App Store URL
 *    mobile.android → Google Play URL
 *    Logos expected at /dashboard-assets/logos/<id>.png. The legacy
 *    APP cards used the same convention so we keep one logo folder.
 *    NOTE: URLs below are best-effort; double-check before shipping
 *    in case the rebrand from Argent X → Ready changed any links. */
const WALLETS = [
  {
    id: 'ready', name: 'READY', category: 'Starknet native',
    title: 'The most popular Starknet wallet.',
    logo: '/dashboard-assets/logos/ready.png',
    headerImage: '/live-apps-headers/ready.jpeg',
    description:
      'The crypto wallet you’ll want to open every day. Swap your favourite tokens and earn DeFi rewards. Over 2 million downloads.',
    website: 'https://www.ready.co/',
    downloadUrl: 'https://www.ready.co/ready-x',
    twitter: 'https://x.com/ready_co',
    what:
      'Self-custodial smart-account wallet purpose-built for Starknet. It speaks the network natively, so strk20 shielding, AVNU swaps, and VESU borrows all feel like one app.',
    extensions: [
      { label: 'Chrome', href: 'https://chromewebstore.google.com/detail/argent-x/dlcobpjiigpikoobohmabehhmhfoodbb' },
      { label: 'Firefox', href: 'https://addons.mozilla.org/firefox/addon/argent-x/' },
    ],
    mobile: {
      ios: 'https://apps.apple.com/app/argent-x-starknet-wallet/id1358741926',
      android: 'https://play.google.com/store/apps/details?id=im.argent.contractwalletclient',
    },
  },
  {
    id: 'xverse', name: 'XVERSE', category: 'Bitcoin · multi-chain',
    title: 'Bitcoin-native, Starknet-aware.',
    logo: '/dashboard-assets/logos/xverse.jpg',
    headerImage: '/live-apps-headers/xverse.jpeg',
    description:
      'Manage all your assets on Starknet in one secure wallet, available for Chrome, Android & iOS.',
    website: 'https://www.xverse.app/',
    twitter: 'https://x.com/xverse',
    what:
      'Self-custodial Bitcoin wallet with native support for Ordinals, Runes, and sBTC. Pairs with strk20 to move BTC into a private balance on Starknet.',
    extensions: [
      { label: 'Chrome', href: 'https://chromewebstore.google.com/detail/xverse-wallet/idnnbdplmphpflfnlkomgpfbpcgelopg' },
      { label: 'Firefox', href: 'https://addons.mozilla.org/firefox/addon/xverse-wallet/' },
    ],
    mobile: {
      ios: 'https://apps.apple.com/app/xverse-bitcoin-web3-wallet/id1552272513',
      android: 'https://play.google.com/store/apps/details?id=com.secretkeylabs.xverse',
    },
  },
]

/* ── Bridges shown inside the Bridge (tab 02) panel. `categories` is
 *    an array because some bridges legitimately fit more than one
 *    pill (e.g. an L2→L2 BTC bridge could be both 'btc' and
 *    'crosschain'). Pills filter by union — a bridge shows when any
 *    of its categories matches any active pill.
 *    Logo + headerImage paths follow the same convention as wallets;
 *    files don't exist yet — onError hides the broken image until
 *    they're dropped in. */
const BRIDGES = [
  {
    id: 'rhinofi', name: 'RHINO.FI', categories: ['thirdparty'], chips: ['Aggregator'],
    title: 'Multi-chain aggregator with Starknet support.',
    logo: '/dashboard-assets/logos/rhinofi.jpg',
    headerImage: '/live-apps-headers/rhinofi.jpeg',
    description:
      'Aggregated bridge/swap with one-click UX across 30+ chains, including Starknet. Routes through the cheapest underlying bridge for the pair.',
    url: 'https://app.rhino.fi/bridge?mode=pay&chainIn=ETHEREUM&chainOut=STARKNET&token=USDT&tokenOut=USDC',
    website: 'https://rhino.fi/',
    twitter: 'https://x.com/rhinofi',
    what:
      'rhino.fi combines a bridge aggregator and a swap aggregator behind a single quote — the app picks the cheapest underlying bridge (often LayerZero / Hyperlane / native bridges) for each route. Starknet sits alongside the EVM L2s as a first-class destination.',
  },
  {
    id: 'layerswap', name: 'LAYERSWAP', categories: ['thirdparty'], chips: ['From CEX', 'Fast'],
    title: 'Move from CEX or any chain into Starknet.',
    logo: '/dashboard-assets/logos/layerswap.jpg',
    headerImage: '/live-apps-headers/layerswap.jpeg',
    description:
      'Bridge from centralised exchanges (Binance, Coinbase, OKX…) and 30+ chains straight to Starknet — skip the L1 hop and the L1 gas.',
    url: 'https://layerswap.io/app?toAsset=USDC&to=STARKNET_MAINNET',
    website: 'https://layerswap.io/',
    twitter: 'https://x.com/layerswap',
    what:
      'Layerswap takes an off-ramp from a CEX or an L1/L2 chain and routes it to Starknet via its own liquidity layer, so users don\'t have to bridge through Ethereum mainnet. Useful when you want to top up a fresh Starknet wallet from an exchange in one transaction.',
  },
  {
    id: 'near-intents', name: 'NEAR INTENTS (via AVNU)', categories: ['crosschain'], chips: ['Fast', 'Intent-based'],
    title: 'Intent-based settlement into Starknet (via AVNU).',
    logo: '/dashboard-assets/logos/near-intents.jpg',
    headerImage: '/live-apps-headers/near-intents.jpeg',
    description:
      'Express what you want (e.g. "USDC on Starknet from BTC on Bitcoin"); a solver network competes to fulfil it at the best rate. Surfaced inside AVNU\'s buy flow.',
    url: 'https://app.avnu.fi/en/buy',
    website: 'https://near.org/intents',
    twitter: 'https://x.com/NEARProtocol',
    what:
      'NEAR Intents is an intent-centric settlement layer: instead of picking a bridge + DEX + chain manually, you sign a desired end-state ("I want X amount of Y on Starknet") and solvers bid to deliver it. AVNU surfaces it as a "buy" path so the user sees a single quote even when the source chain is Bitcoin, Solana, or an EVM L2.',
  },
  {
    id: 'hyperlane', name: 'HYPERLANE', categories: ['crosschain'], chips: ['Multi-chain'],
    title: 'Permissionless interchain messaging.',
    logo: '/dashboard-assets/logos/hyperlane.jpg',
    headerImage: '/live-apps-headers/hyperlane.jpeg',
    description:
      'Generalised cross-chain bridge built on Hyperlane\'s interchain security modules. Move tokens and messages between Starknet and 100+ chains.',
    url: 'https://nexus.hyperlane.xyz/?destination=starknet&destinationToken=USDC',
    website: 'https://www.hyperlane.xyz/',
    twitter: 'https://x.com/hyperlane',
    what:
      'Hyperlane is interoperability infrastructure: any chain can deploy mailbox contracts and pick its own Interchain Security Module (ISM) — proof-of-stake validators, ZK light clients, or hybrid setups. Starknet integration lets tokens (warp routes) and arbitrary messages flow between Starknet and the broader Hyperlane network.',
  },
  {
    id: 'rocketx', name: 'RocketX', categories: ['thirdparty'], chips: ['Aggregator'],
    title: 'Cross-chain swap aggregator with Starknet support.',
    logo: '/dashboard-assets/logos/rocketx.jpg',
    headerImage: '/live-apps-headers/rocketx.jpeg',
    description:
      'Multi-chain swap aggregator routing across 100+ chains and 1M+ token pairs. Pick a route and let RocketX execute it across the underlying bridges and DEXs.',
    url: 'https://app.rocketx.exchange/swap?rx_t=dark',
    website: 'https://rocketx.exchange/',
    twitter: 'https://x.com/RocketXexchange',
    what:
      'RocketX is a meta-aggregator: it picks the cheapest path across underlying bridges and DEXs for cross-chain swaps. Starknet is a supported destination, so users can bridge from CEXs and L1/L2s through RocketX in a single transaction.',
  },
  {
    id: 'starkgate', name: 'STARKGATE', categories: ['native'], chips: ['Official'],
    title: 'The official Ethereum ↔ Starknet bridge.',
    logo: '/dashboard-assets/logos/starkgate.png',
    headerImage: '/live-apps-headers/starkgate.png',
    description:
      'Maintained by StarkWare. Move ETH, STRK, USDC and the supported ERC-20s between Ethereum L1 and Starknet, secured by the same STARK proofs that settle the rollup itself.',
    url: 'https://starkgate.starknet.io/',
    website: 'https://starkgate.starknet.io/',
    twitter: 'https://x.com/Starknet',
    what:
      'StarkGate is the canonical L1↔L2 bridge for Starknet. Deposits lock funds in an L1 contract that mints the same asset on Starknet; withdrawals burn on L2 and release on L1 once the proof settles. There is no third-party validator set — trust collapses to the same prover and L1 verifier securing every Starknet transaction.',
  },
  {
    id: 'strkbtc', name: 'strkBTC', categories: ['btc'], chips: ['Official'],
    title: 'Wrap BTC into Starknet as strkBTC.',
    logo: '/dashboard-assets/logos/strkbtc.png',
    headerImage: '/live-apps-headers/strkbtc.png',
    description:
      'Deposit native Bitcoin and receive strkBTC on Starknet — a 1:1 wrapped representation you can lend, swap, or shield through strk20.',
    url: 'https://strkbtc.io/?tab=deposit',
    website: 'https://strkbtc.io/',
    twitter: 'https://x.com/Starknet',
    what:
      'strkBTC is the Bitcoin entry point most Starknet DeFi protocols already accept as collateral. The bridge takes a BTC deposit on the Bitcoin chain and mints an equivalent amount of strkBTC on Starknet; redemptions burn strkBTC and release BTC on the way out. Pairs naturally with shielding through strk20 once the BTC value is on-chain.',
  },
  {
    id: 'stargate', name: 'STARGATE', categories: ['crosschain'], chips: ['Multi-chain', 'Fast'],
    title: 'Native asset bridging across chains.',
    logo: '/dashboard-assets/logos/stargate.png',
    headerImage: '/live-apps-headers/stargate.png',
    description:
      'Cross-chain bridge for native ETH, USDC and other assets. Unified liquidity pools — no wrapped intermediaries on either side.',
    url: 'https://stargate.finance/?dstChain=starknet&dstToken=0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb',
    website: 'https://stargate.finance/',
    twitter: 'https://x.com/StargateFinance',
    what:
      'Stargate is a native-asset cross-chain bridge built on top of LayerZero messaging. Instead of issuing wrapped variants, it uses unified liquidity pools and the Δ ("Delta") algorithm so a token on the source chain comes out as the same canonical token on the destination — Starknet included. Useful when you need real ETH or real USDC on the other side, not a wrapper.',
  },
  {
    id: 'atomiq', name: 'ATOMIQ', categories: ['btc'], chips: ['Trustless'],
    title: 'Trust-minimised Bitcoin ↔ Starknet swaps.',
    logo: '/dashboard-assets/logos/atomiq.jpg',
    headerImage: '/live-apps-headers/atomiq.jpeg',
    description:
      'Atomic swaps between BTC (mainchain + Lightning) and Starknet assets — no custodial wrapper, no validator set in the middle.',
    url: 'https://app.atomiq.exchange/',
    website: 'https://atomiq.exchange/',
    twitter: 'https://x.com/atomiqlabs',
    what:
      'Atomiq uses HTLC-style atomic swaps so each cross-chain trade either settles on both sides or refunds on both sides — there is no period where the counterparty holds your funds unilaterally. Supports BTC mainchain and Lightning on the Bitcoin side, plus the major Starknet assets, and the executor logic is fully on-chain on Starknet.',
  },
  {
    id: 'garden', name: 'GARDEN', categories: ['btc'], chips: ['Fast', 'Trustless'],
    title: 'Bitcoin ↔ Starknet, in one click.',
    logo: '/dashboard-assets/logos/garden.jpg',
    headerImage: '/live-apps-headers/garden.png',
    description:
      'Cross-chain BTC bridge built on atomic swaps. Bridge native BTC into Starknet (and out again) without a wrapped-asset middle layer.',
    url: 'https://app.garden.finance/bridge/starknet?input-chain=bitcoin&input-asset=BTC&output-asset=WBTC',
    website: 'https://garden.finance/',
    twitter: 'https://x.com/garden_finance',
    what:
      'Garden orchestrates atomic swaps between Bitcoin and EVM/L2 chains, with Starknet support as part of its multi-chain coverage. A solver network sources liquidity on both sides so the user just sees a quote and a single confirmation — under the hood each trade is still an HTLC swap, not a custodial mint.',
  },
]

/* ── Live apps shown inside the Apps (tab 04) panel. Same card +
 *    slide-panel pattern as the bridges; chips carry the category
 *    label (DEX, Lending, Staking, Yield) since these apps only fit
 *    one each. Slide panel adds Publicly/Privately bullet lists to
 *    surface what each app does in public vs. private mode. */
const APPS = [
  {
    id: 'avnu', name: 'AVNU', chips: ['DEX'],
    title: 'Swap without revealing your trade.',
    logo: '/dashboard-assets/logos/avnu.png',
    headerImage: '/live-apps-headers/avnu.jpeg',
    url: 'https://app.avnu.fi/',
    website: 'https://avnu.fi/',
    twitter: 'https://x.com/avnu_fi',
    what:
      'A DEX aggregator that finds the best swap rates by routing trades across Starknet liquidity sources.',
    publicly: [
      'Swap any Starknet token for any other token',
      'Multi-venue routing for the best price',
      'Sponsored gas on supported flows',
    ],
    privately: [
      "Swap from inside the privacy pool — AVNU's private executor splits the route and settles into a new private note",
      'No on-chain link between your input and output tokens',
    ],
  },
  {
    id: 'vesu', name: 'VESU', chips: ['Lending', 'Borrowing'],
    title: 'Lend and borrow, privately.',
    logo: '/dashboard-assets/logos/vesu.png',
    headerImage: '/live-apps-headers/vesu.jpeg',
    url: 'https://vesu.xyz/pro/earn',
    website: 'https://vesu.xyz/lite/markets',
    twitter: 'https://x.com/vesuxyz',
    what:
      'A decentralized lending and borrowing protocol where users can earn yield on deposits or borrow against their assets.',
    publicly: [
      'Supply STRK / USDC / ETH to earn lending yield',
      'Borrow against your collateral',
      'Withdraw or repay any time',
    ],
    privately: [
      'Supply assets privately to earn yield',
      'Borrow against your collateral privately',
    ],
  },
  {
    id: 'endur', name: 'ENDUR', chips: ['Staking'],
    title: 'Stake STRK and BTC, keep your privacy.',
    logo: '/dashboard-assets/logos/endur.png',
    headerImage: '/live-apps-headers/endur.jpeg',
    url: 'https://app.endur.fi/',
    website: 'https://endur.fi/',
    twitter: 'https://x.com/endurfi',
    what:
      'Liquid staking for STRK and BTC, letting you earn rewards while keeping your assets liquid.',
    publicly: [
      'Stake STRK → xSTRK',
      'Stake BTC (WBTC / strkBTC / SolvBTC / LBTC / tBTC)',
      'Transfer or use the LST as collateral elsewhere',
    ],
    privately: [
      'Private staking',
      'Private staking with strkBTC',
    ],
  },
  {
    id: 'troves', name: 'TROVES', chips: ['Yield'],
    title: 'Automated yield, shielded vaults.',
    logo: '/dashboard-assets/logos/troves.png',
    headerImage: '/live-apps-headers/troves.jpeg',
    url: 'https://app.troves.fi/',
    website: 'https://www.troves.fi/',
    twitter: 'https://x.com/trovesfi',
    what:
      'Earn passive income with automated vaults that optimize yields across Starknet DeFi.',
    publicly: [
      'Deposit into a yield strategy of your choice',
      'Exit the position at any time',
      'Hold vault shares like any other ERC20',
    ],
    privately: [
      'Yield-strategy deposits',
      'Yield earning, privately',
    ],
  },
  {
    id: 'forgeyields', name: 'FORGEYIELDS', chips: ['Yield'],
    title: 'Forge your yield strategy.',
    logo: '/dashboard-assets/logos/forgeyields.jpg',
    headerImage: '/live-apps-headers/forgeyields.jpeg',
    url: 'https://app.forgeyields.com/',
    website: 'https://forgeyields.com/',
    twitter: 'https://x.com/forgeyields',
    what:
      'Build and execute yield strategies on Starknet — compose across the protocols you already use and let ForgeYields handle the routing.',
    publicly: [
      'Compose multi-step yield strategies',
      'Auto-route into the best venue per step',
      'Withdraw to your own wallet any time',
    ],
    privately: [
      'Run strategies privately',
      'Yield earned and rebalanced without revealing the position',
    ],
  },
  {
    id: 'opus', name: 'OPUS', chips: ['CDP', 'Stablecoin'],
    title: 'Mint stablecoins against your collateral.',
    logo: '/dashboard-assets/logos/opus.jpg',
    headerImage: '/live-apps-headers/opus.jpeg',
    url: 'https://app.opus.money/',
    website: 'https://opus.money/',
    twitter: 'https://x.com/opus_money',
    what:
      'A CDP-style protocol that lets you lock collateral on Starknet and mint a USD-pegged stablecoin against it. Repay to unlock.',
    publicly: [
      'Lock STRK / ETH / wBTC as collateral',
      'Mint CASH against your position',
      'Repay any time to unlock',
    ],
    privately: [
      'Private CDP — collateral lock and stablecoin mint',
      'Mint position not linkable to your public balance',
    ],
  },
  {
    id: 'ekubo', name: 'EKUBO', chips: ['DEX', 'Liquidity'],
    title: 'Concentrated liquidity, on Starknet.',
    logo: '/dashboard-assets/logos/ekubo.png',
    headerImage: '/live-apps-headers/ekubo.jpeg',
    url: 'https://app.ekubo.org/',
    website: 'https://ekubo.org/',
    twitter: 'https://x.com/ekuboprotocol',
    what:
      "Starknet's concentrated-liquidity AMM. Single-tick deep liquidity, extension hooks, and the main on-chain swap venue for STRK-paired assets.",
    publicly: [
      'Swap any Starknet token at the best on-chain price',
      'Provide concentrated liquidity for fees',
      'Public routes via the Ekubo router',
    ],
    privately: [
      'Swap privately through the Ekubo anonymizer',
      'Output settles into a new private note',
    ],
  },
  {
    id: 'arcx', name: 'ARCX', chips: ['Perps'],
    title: 'Perpetuals, native to Starknet.',
    logo: '/dashboard-assets/logos/arcx.png',
    headerImage: '/live-apps-headers/arcx.jpeg',
    url: 'https://app.arcx.money/',
    website: 'https://arcx.money/',
    twitter: 'https://x.com/arcx_money',
    what:
      'Perpetual futures venue running natively on Starknet, with the matching engine settled on-chain.',
    publicly: [
      'Open long / short positions on listed markets',
      'Cross-margin against your collateral',
      'Settle PnL in USDC',
    ],
    privately: [
      'Open / close positions privately',
      'Collateral and PnL not linked to your public balance',
    ],
  },
]

/* Route presets for the bridge panel — intent-based labels rendered
 * inside a single dropdown trigger. Each route maps to the specific
 * bridge IDs that match that intent. Single-select: when a route is
 * active the grid filters to its bridges; clearing the route (or
 * picking "All bridges") shows everything. */
/* Labels are verb phrases that complete the sentence
 * "I want to ___". The dropdown options render the phrase alone; the
 * trigger prepends "I want to " when a route is active so the
 * selected state reads as a full sentence. */
const BRIDGE_ROUTES = [
  { id: 'eth',        label: 'funds from Ethereum (15 min)',       bridgeIds: ['starkgate'] },
  { id: 'btc',        label: 'BTC to Starknet (5-30 min)',          bridgeIds: ['strkbtc', 'atomiq', 'garden'] },
  { id: 'cex',        label: 'from an exchange (2-10 min)',         bridgeIds: ['layerswap'] },
  { id: 'multichain', label: 'now (1-3 min)',                       bridgeIds: ['hyperlane', 'stargate', 'rhinofi', 'rocketx', 'near-intents', 'layerswap'] },
]

/* Intent-based routes for the Apps (04) panel — same single-select
 * dropdown UX as bridges. Label is the verb phrase that completes
 * "I want to ___". appIds filter the grid: pick swap → only AVNU
 * shows, etc. No-route (null) shows every app plus the two
 * "Coming soon" placeholder cards. */
const APP_ROUTES = [
  { id: 'swap',   label: 'swap tokens privately',              appIds: ['avnu']   },
  { id: 'stake',  label: 'stake STRK or BTC privately',        appIds: ['endur']  },
  { id: 'lend',   label: 'lend and borrow privately',          appIds: ['vesu']   },
  { id: 'yield',  label: 'earn from yield strategies privately', appIds: ['troves', 'forgeyields'] },
]

/* Detect mobile via UA on mount so the wallet slide-panel can swap
 * browser-extension links for App Store / Play Store links. We DON'T
 * try to re-render on resize — UA is set once. Note: the rest of the
 * page is still desktop-only by design until a mobile pass lands. */
function useIsMobile() {
  const [isMobile, setIsMobile] = useState(false)
  useEffect(() => {
    if (typeof navigator !== 'undefined') {
      setIsMobile(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
    }
  }, [])
  return isMobile
}

/* Drag-to-dismiss for the slide panels when they render as a bottom
 * sheet on narrow viewports (≤640px, controlled by CSS in the <style>
 * block at the end of LiveApps). Returns touch handlers + a style
 * override the panel spreads onto its <aside>.
 *
 * Behavior:
 * - Only intercepts when the sheet is scrolled to the top — otherwise
 *   the drag is the user scrolling the panel content.
 * - During the drag, transform: translateY(dy) tracks the finger; the
 *   keyframe animation is suppressed so the two don't fight.
 * - On release: <100px snap back to 0; ≥100px call requestClose(). The
 *   parent's `closing` flag then arms the dismiss path — useEffect
 *   below rides the transform out to the bottom of the viewport via
 *   the same CSS transition, and onTransitionEnd unmounts the panel.
 *
 * On desktop (>640px) the CSS @media keeps the aside as a side panel
 * with no drag handle, and these handlers do nothing meaningful since
 * touch events aren't fired. */
function useBottomSheetDrag(requestClose, closing) {
  const [dragY, setDragY] = useState(0)
  // phase: 'idle' = no interaction, the inline animation prop runs
  // unimpeded; 'dragging' = finger is tracking translateY; 'snapping' =
  // releasing back to 0 via transition after a sub-threshold drag.
  const [phase, setPhase] = useState('idle')
  const startYRef = useRef(0)
  const skipRef = useRef(false)
  const snapTimerRef = useRef(null)

  // When the parent flips closing=true mid-drag (or after a drag past
  // threshold), slide the rest of the way out via the CSS transition
  // instead of letting the keyframe animation snap back to translateY(0).
  useEffect(() => {
    if (closing && dragY > 0 && typeof window !== 'undefined') {
      setDragY(window.innerHeight)
    }
  }, [closing])

  useEffect(() => () => {
    if (snapTimerRef.current) clearTimeout(snapTimerRef.current)
  }, [])

  const handlers = {
    onTouchStart: (e) => {
      const el = e.currentTarget
      if (el.scrollTop > 4) { skipRef.current = true; return }
      skipRef.current = false
      startYRef.current = e.touches[0].clientY
      setPhase('dragging')
    },
    onTouchMove: (e) => {
      if (skipRef.current || phase !== 'dragging') return
      const dy = e.touches[0].clientY - startYRef.current
      if (dy > 0) setDragY(dy)
    },
    onTouchEnd: () => {
      if (skipRef.current) { skipRef.current = false; return }
      if (dragY > 100) {
        // Dismiss: parent's `closing` flag will arm the transform-out via
        // the effect above. Stay in 'dragging' phase so the override
        // style (which sets transition + suppresses keyframes) keeps
        // applying while the transform rides out.
        requestClose()
      } else {
        // Snap back via the same transition. Hold 'snapping' phase long
        // enough for the transition to complete, then go idle so the
        // inline keyframe animation prop is restored to its raw state.
        setPhase('snapping')
        setDragY(0)
        if (snapTimerRef.current) clearTimeout(snapTimerRef.current)
        snapTimerRef.current = setTimeout(() => {
          setPhase('idle')
          snapTimerRef.current = null
        }, 320)
      }
    },
  }

  // CRITICAL: when no interaction is happening, return ONLY touchAction.
  // Returning `animation: undefined` here would let React clear the
  // inline `animation: 'la-slide-in/out ...'` prop (React maps undefined
  // values to '' on the DOM style), which kills the open/close animations
  // and prevents onAnimationEnd from ever firing → click-to-close dies.
  const isInteracting = phase !== 'idle' || dragY > 0
  const style = isInteracting
    ? {
        transform: dragY ? `translateY(${dragY}px)` : 'translateY(0)',
        transition: phase === 'dragging' ? 'none' : 'transform .3s cubic-bezier(.4, 0, .2, 1)',
        // Suppress the keyframe animation while we're hand-driving the
        // transform via transition. By the time someone interacts, the
        // entry animation has already painted to its final frame, so
        // overriding here doesn't lose anything visible.
        animation: 'none',
        touchAction: 'pan-y',
      }
    : { touchAction: 'pan-y' }

  return { handlers, style, dragY }
}

/* Inline SVG icon set — pure SVG, stroke/fill via currentColor so the
 * parent's color drives them. Used by the wallet cards + slide panel. */
const Icon = {
  x: (p) => (
    <svg viewBox="0 0 16 16" width={p.size || 14} height={p.size || 14} fill="currentColor" aria-hidden="true">
      <path d="M12.2 1.5h2.3l-5 5.7 5.9 7.8h-4.6L7.2 9.3l-4.1 5.7H.7l5.4-6.1L.5 1.5h4.7l3.2 4.3 3.8-4.3zm-.8 11.6h1.3L4.6 2.9H3.2l8.2 10.2z" />
    </svg>
  ),
  globe: (p) => (
    <svg viewBox="0 0 16 16" width={p.size || 14} height={p.size || 14} fill="none" stroke="currentColor" strokeWidth="1.2" aria-hidden="true">
      <circle cx="8" cy="8" r="6.5" />
      <ellipse cx="8" cy="8" rx="2.6" ry="6.5" />
      <line x1="1.5" y1="8" x2="14.5" y2="8" />
    </svg>
  ),
  discord: (p) => (
    <svg viewBox="0 0 16 16" width={p.size || 14} height={p.size || 14} fill="currentColor" aria-hidden="true">
      <path d="M13.6 3.3a12 12 0 0 0-3-1l-.1.3a11 11 0 0 0-5 0L5.4 2.3a12 12 0 0 0-3 1A12.5 12.5 0 0 0 1 11.7a12 12 0 0 0 3.7 1.9l.8-1.1c-.6-.2-1.2-.5-1.7-.9.1-.1.3-.2.4-.3a8.7 8.7 0 0 0 7.6 0c.1.1.3.2.4.3-.5.4-1.1.7-1.7.9l.8 1.1A12 12 0 0 0 15 11.7a12.5 12.5 0 0 0-1.4-8.4zM6 9.7c-.7 0-1.3-.7-1.3-1.5S5.3 6.7 6 6.7s1.3.7 1.3 1.5S6.7 9.7 6 9.7zm4 0c-.7 0-1.3-.7-1.3-1.5s.6-1.5 1.3-1.5 1.3.7 1.3 1.5-.6 1.5-1.3 1.5z" />
    </svg>
  ),
}

function GhostButton({ children, onClick, href, style = {} }) {
  const base = {
    fontFamily: 'var(--mono)', fontSize: 11.5, letterSpacing: '0.14em',
    textTransform: 'uppercase', color: 'var(--text)',
    background: 'transparent', border: '1px solid var(--line)',
    borderRadius: 2, padding: '0 14px', height: 32,
    display: 'inline-flex', alignItems: 'center', gap: 6,
    cursor: 'pointer', textDecoration: 'none',
    transition: 'border-color .15s, background .15s, color .15s',
    ...style,
  }
  if (href) {
    return (
      <a href={href} target="_blank" rel="noopener noreferrer"
         onClick={(e) => e.stopPropagation()}
         onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--green)'; e.currentTarget.style.color = 'var(--green)' }}
         onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.color = 'var(--text)' }}
         style={base}>{children}</a>
    )
  }
  return (
    <button onClick={(e) => { e.stopPropagation(); onClick?.(e) }}
            onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--green)'; e.currentTarget.style.color = 'var(--green)' }}
            onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.color = 'var(--text)' }}
            style={base}>{children}</button>
  )
}

function SocialLink({ href, children }) {
  if (!href) return null
  return (
    <a href={href} target="_blank" rel="noopener noreferrer"
       onClick={(e) => e.stopPropagation()}
       onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--green)' }}
       onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--dim)' }}
       style={{
         color: 'var(--dim)', textDecoration: 'none',
         display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
         padding: 6, transition: 'color .15s',
       }} aria-label="external link">{children}</a>
  )
}

function WalletCard({ wallet, onPreview }) {
  const [hover, setHover] = useState(false)
  return (
    <div
      className="la-product-card"
      onClick={() => onPreview(wallet)}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPreview(wallet) } }}
      style={{
        position: 'relative', overflow: 'hidden',
        background:
          'radial-gradient(ellipse 60% 60% at 100% 0%, rgba(197,52,0,0.08), transparent 55%), ' +
          'linear-gradient(180deg, rgba(20,20,20,0.85) 0%, rgba(10,10,10,0.9) 100%)',
        border: '1px solid var(--line)',
        borderTop: '2px solid var(--orange)',
        borderRadius: 5,
        display: 'flex', flexDirection: 'column',
        cursor: 'pointer',
        transition: 'transform .25s ease-out, box-shadow .25s ease-out',
        transform: hover ? 'translateY(-3px)' : 'none',
        boxShadow: hover ? '0 22px 60px -28px rgba(197,52,0,0.55)' : 'none',
        minHeight: 340,
      }}
    >
      {/* Header band — wallet brand image as background, dark gradient
          overlay layered on top for contrast, then the category tag /
          name / logo tile rendered above both. Layer order in the
          shorthand: the gradient is FIRST → it paints last (on top of
          the image). minHeight gives the image visible breathing room
          above the bottom content row. */}
      <div className="la-product-card__header" style={{
        position: 'relative',
        padding: '20px 22px 24px',
        minHeight: 200,
        borderBottom: '1px solid var(--line)',
        background: wallet.headerImage
          ? `linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.78) 100%), url(${wallet.headerImage}) center/cover no-repeat #0a0a0a`
          : 'rgba(0,0,0,0.18)',
        display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 18,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div style={{
            width: 52, height: 52, flexShrink: 0,
            background: 'var(--bg)',
            border: '1px solid var(--line)',
            borderRadius: 4,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            overflow: 'hidden',
          }}>
            <img
              src={wallet.logo}
              alt={`${wallet.name} logo`}
              onError={(e) => { e.currentTarget.style.display = 'none' }}
              style={{ width: '78%', height: '78%', objectFit: 'contain' }}
            />
          </div>
          <div className="la-product-card__name-block">
            <div style={{
              fontFamily: 'var(--display)', fontWeight: 800,
              fontSize: 'clamp(24px,2.6vw,34px)', lineHeight: 1.0,
              letterSpacing: '-0.025em', textTransform: 'uppercase',
              color: 'var(--text)',
            }}>{wallet.name}</div>
            {/* Category chip — hidden on desktop (the banner banner does
                the heavy lifting up there), surfaced on mobile beneath
                the wallet name to mirror the reference card layout. */}
            <div className="la-product-card__category" style={{
              display: 'none',
              marginTop: 6,
              fontFamily: 'var(--mono)', fontSize: 10.5,
              letterSpacing: '0.18em', textTransform: 'uppercase',
              color: 'var(--orange)',
            }}>[ {wallet.category} ]</div>
          </div>
        </div>
      </div>

      {/* Body */}
      <div className="la-product-card__body" style={{ padding: '18px 20px 14px', flex: 1 }}>
        <p style={{
          margin: 0, fontFamily: 'var(--mono)', fontSize: 12,
          lineHeight: 1.7, letterSpacing: '0.04em', color: 'var(--dim)',
        }}>{wallet.description}</p>
      </div>

      {/* Footer — socials + Preview */}
      <div className="la-card-footer" style={{
        padding: '12px 16px', borderTop: '1px solid var(--line)',
        background: 'rgba(0,0,0,0.18)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10,
      }}>
        <div className="la-card-footer__socials" style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
          {wallet.twitter && <SocialLink href={wallet.twitter}><Icon.x /></SocialLink>}
          {wallet.website && <SocialLink href={wallet.website}><Icon.globe /></SocialLink>}
        </div>
        <div className="la-card-footer__actions" style={{ display: 'flex', gap: 8 }}>
          {/* TUTORIAL is a placeholder for now — no href, no onClick. */}
          <GhostButton>Tutorial</GhostButton>
          <GhostButton onClick={() => onPreview(wallet)}>Preview</GhostButton>
          {/* DOWNLOAD prefers a wallet-specific install page
              (wallet.downloadUrl) and falls back to wallet.website.
              External link, opens in a new tab. */}
          {(wallet.downloadUrl || wallet.website) && (
            <GhostButton href={wallet.downloadUrl || wallet.website}>Download</GhostButton>
          )}
        </div>
      </div>
    </div>
  )
}

function WalletSlidePanel({ wallet, onClose }) {
  const isMobile = useIsMobile()
  const [closing, setClosing] = useState(false)
  const requestClose = () => { if (!closing) setClosing(true) }
  const sheet = useBottomSheetDrag(requestClose, closing)
  const onAnimationEnd = (e) => {
    if (closing && (e.animationName === 'la-slide-out' || e.animationName === 'la-sheet-down')) onClose()
  }
  // Drag-dismiss path: closing=true + dragY>0 rides transform to the
  // bottom of the viewport via CSS transition. When it settles, unmount.
  const onTransitionEnd = (e) => {
    if (closing && e.propertyName === 'transform' && sheet.dragY > 0) onClose()
  }

  useEffect(() => {
    const prev = document.body.style.overflow
    document.body.style.overflow = 'hidden'
    return () => { document.body.style.overflow = prev }
  }, [])

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') requestClose() }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [closing])

  return (
    <>
      <div onClick={requestClose} style={{
        position: 'fixed', inset: 0, zIndex: 90,
        background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
        animation: closing ? 'la-fade-out .26s ease-in forwards' : 'la-fade-in .22s ease-out',
      }} />
      <aside
        className="la-side-panel"
        data-closing={closing ? 'true' : undefined}
        role="dialog" aria-label={`${wallet.name} details`}
        onAnimationEnd={onAnimationEnd}
        onTransitionEnd={onTransitionEnd}
        {...sheet.handlers}
        style={{
          position: 'fixed', top: 72, right: 0, bottom: 0, zIndex: 91,
          width: 'min(560px, 92vw)',
          background: 'var(--bg)',
          border: '1px solid var(--line)', borderRight: 'none',
          borderTopLeftRadius: 8,
          color: 'var(--text)', overflowY: 'auto',
          boxShadow: '0 0 60px rgba(0,0,0,0.45)',
          animation: closing
            ? 'la-slide-out .28s cubic-bezier(.5, 0, .75, 0) forwards'
            : 'la-slide-in .32s cubic-bezier(.2, .7, .2, 1)',
          ...sheet.style,
        }}
      >
        <div className="la-sheet-handle" aria-hidden="true" />
        <button onClick={requestClose} aria-label="Close panel" style={{
          position: 'absolute', top: 16, right: 16, zIndex: 2,
          background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(6px)',
          WebkitBackdropFilter: 'blur(6px)',
          border: '1px solid rgba(255,255,255,0.12)',
          color: 'var(--text)', borderRadius: 2, padding: '0 12px', height: 30,
          fontFamily: 'var(--mono)', fontSize: 11, cursor: 'pointer',
          letterSpacing: '0.08em', textTransform: 'uppercase',
        }}>Close ✕</button>

        {/* Full-bleed banner — same pattern as the legacy app slide
            panel. Profile-picture-style logo below overlaps the
            bottom edge of the banner via negative marginTop. */}
        <div style={{
          position: 'relative', zIndex: 0,
          width: '100%',
          height: 'clamp(130px, 22vw, 180px)',
          background: wallet.headerImage
            ? `#0a0a0a url(${wallet.headerImage}) center/cover no-repeat`
            : 'linear-gradient(135deg, var(--bg-1), var(--bg-2))',
          borderBottom: '1px solid var(--line)',
          borderTopLeftRadius: 8,
        }} />

        <div style={{ padding: '0 clamp(28px,4vw,44px) clamp(28px,4vw,44px)', position: 'relative' }}>
          {/* Profile-picture-style logo — overlaps the bottom of the
              banner. Explicit z-index keeps it above the banner. */}
          <div style={{
            position: 'relative', zIndex: 2,
            width: 144, height: 144, marginTop: -72,
            border: '4px solid var(--bg)', borderRadius: 8,
            background: 'var(--bg-1)', overflow: 'hidden',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: '0 12px 32px rgba(0,0,0,0.5)',
          }}>
            <img
              src={wallet.logo}
              alt={`${wallet.name} logo`}
              onError={(e) => { e.currentTarget.style.display = 'none' }}
              style={{ width: '74%', height: '74%', objectFit: 'contain' }}
            />
          </div>

          <div style={{
            marginTop: 18, fontFamily: 'var(--mono)', fontSize: 11,
            letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--orange)',
          }}>
            [ {wallet.category} ]
          </div>

          <div style={{
            marginTop: 8, fontFamily: 'var(--display)', fontWeight: 800,
            fontSize: 'clamp(28px,3.4vw,44px)', letterSpacing: '-0.025em',
            lineHeight: 1.0, textTransform: 'uppercase',
          }}>{wallet.name}</div>

          <p style={{
            marginTop: 22, fontFamily: 'var(--mono)', fontSize: 13,
            lineHeight: 1.7, letterSpacing: '0.04em', color: 'var(--text)',
          }}>{wallet.what}</p>

          {/* Install links — desktop shows browser extensions, mobile
              shows the App Store / Google Play targets. Pure UA split
              done once on mount via useIsMobile. */}
          <div style={{ marginTop: 28 }}>
            <div style={{
              fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.14em',
              textTransform: 'uppercase', color: 'var(--green)', marginBottom: 12,
            }}>
              {isMobile ? 'Get the app' : 'Install the extension'}
            </div>
            {/* Single row, same shape as the bridge slide-in CTA:
                socials on the left, install/tutorial buttons on the
                right, pushed apart with justify-content: space-between.
                Wraps as a unit on narrower panels. */}
            <div style={{
              display: 'flex', alignItems: 'center',
              gap: 10, flexWrap: 'wrap',
              justifyContent: 'space-between',
            }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                {wallet.website && <SocialLink href={wallet.website}><Icon.globe size={20} /></SocialLink>}
                {wallet.twitter && <SocialLink href={wallet.twitter}><Icon.x     size={20} /></SocialLink>}
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
                {isMobile ? (
                  <>
                    {wallet.mobile?.ios && (
                      <GhostButton href={wallet.mobile.ios}>App Store <span style={{ marginLeft: 2 }}>↗</span></GhostButton>
                    )}
                    {wallet.mobile?.android && (
                      <GhostButton href={wallet.mobile.android}>Google Play <span style={{ marginLeft: 2 }}>↗</span></GhostButton>
                    )}
                  </>
                ) : (
                  <>
                    {/* TUTORIAL placeholder — no href, no onClick. */}
                    <GhostButton>Tutorial</GhostButton>
                    {/* DOWNLOAD uses the same wallet-specific install
                        page the card footer's Download button uses
                        (downloadUrl, falling back to website). */}
                    {(wallet.downloadUrl || wallet.website) && (
                      <GhostButton href={wallet.downloadUrl || wallet.website}>Download</GhostButton>
                    )}
                  </>
                )}
              </div>
            </div>
          </div>
        </div>
      </aside>
    </>
  )
}

/* ── Bridge UI ────────────────────────────────────────────────────
 * Same card / slide-panel pattern as wallets, but the panel has no
 * install block and the filter row supports multi-select pills. */

function BridgeRouteSelect({ selectedRoute, onSelect }) {
  const [open, setOpen] = useState(false)
  const wrapRef = useRef(null)

  // Click-outside + Escape close the menu when it's open. Listeners
  // only attach while open so we don't pay for them otherwise.
  useEffect(() => {
    if (!open) return
    const onClick = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false)
    }
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
    document.addEventListener('mousedown', onClick)
    document.addEventListener('keydown', onKey)
    return () => {
      document.removeEventListener('mousedown', onClick)
      document.removeEventListener('keydown', onKey)
    }
  }, [open])

  const current = selectedRoute ? BRIDGE_ROUTES.find((r) => r.id === selectedRoute) : null

  return (
    <div ref={wrapRef} className="bridge-route" data-open={open ? 'true' : 'false'}>
      <button
        type="button"
        className="bridge-route__trigger"
        aria-haspopup="listbox"
        aria-expanded={open}
        onClick={() => setOpen((o) => !o)}
      >
        <span className="bridge-route__label">
          {current ? `I want to bridge ${current.label}` : 'I want to bridge…'}
        </span>
        <span className="bridge-route__caret" aria-hidden="true">▾</span>
      </button>

      {/* Menu is always rendered so the open/close transition runs in
          both directions. pointer-events is off when closed so it
          doesn't block clicks. */}
      <div className="bridge-route__menu" role="listbox">
        {selectedRoute && (
          <button
            type="button"
            className="bridge-route__option bridge-route__option--reset"
            onClick={() => { onSelect(null); setOpen(false) }}
          >
            [ All bridges ]
          </button>
        )}
        {BRIDGE_ROUTES.map((r) => {
          const isOn = r.id === selectedRoute
          return (
            <button
              key={r.id}
              type="button"
              role="option"
              aria-selected={isOn}
              data-on={isOn ? 'true' : 'false'}
              className="bridge-route__option"
              onClick={() => {
                // Toggle: clicking the active option clears it.
                onSelect(isOn ? null : r.id)
                setOpen(false)
              }}
            >
              {r.label}
            </button>
          )
        })}
      </div>
    </div>
  )
}

/* Apps tab intent dropdown — same single-select pattern as
 * BridgeRouteSelect (reuses the `bridge-route__*` CSS classes, which
 * are styling-only, not bridge-specific). Trigger reads
 * "I want to ___"; reset option clears the filter. */
function AppRouteSelect({ selectedRoute, onSelect }) {
  const [open, setOpen] = useState(false)
  const wrapRef = useRef(null)

  useEffect(() => {
    if (!open) return
    const onClick = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false)
    }
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
    document.addEventListener('mousedown', onClick)
    document.addEventListener('keydown', onKey)
    return () => {
      document.removeEventListener('mousedown', onClick)
      document.removeEventListener('keydown', onKey)
    }
  }, [open])

  const current = selectedRoute ? APP_ROUTES.find((r) => r.id === selectedRoute) : null

  return (
    <div ref={wrapRef} className="bridge-route" data-open={open ? 'true' : 'false'}>
      <button
        type="button"
        className="bridge-route__trigger"
        aria-haspopup="listbox"
        aria-expanded={open}
        onClick={() => setOpen((o) => !o)}
      >
        <span className="bridge-route__label">
          {current ? `I want to ${current.label}` : 'I want to…'}
        </span>
        <span className="bridge-route__caret" aria-hidden="true">▾</span>
      </button>

      <div className="bridge-route__menu" role="listbox">
        {selectedRoute && (
          <button
            type="button"
            className="bridge-route__option bridge-route__option--reset"
            onClick={() => { onSelect(null); setOpen(false) }}
          >
            [ All apps ]
          </button>
        )}
        {APP_ROUTES.map((r) => {
          const isOn = r.id === selectedRoute
          return (
            <button
              key={r.id}
              type="button"
              role="option"
              aria-selected={isOn}
              data-on={isOn ? 'true' : 'false'}
              className="bridge-route__option"
              onClick={() => {
                onSelect(isOn ? null : r.id)
                setOpen(false)
              }}
            >
              {r.label}
            </button>
          )
        })}
      </div>
    </div>
  )
}

function BridgeCard({ bridge, onPreview }) {
  const [hover, setHover] = useState(false)
  return (
    <div
      className="la-product-card"
      onClick={() => onPreview(bridge)}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPreview(bridge) } }}
      style={{
        position: 'relative', overflow: 'hidden',
        // min-width: 0 lets the card shrink inside a 1fr grid column —
        // without it, grid items use their min-content width and can
        // overflow when the column becomes narrower than the card's
        // intrinsic minimum (e.g. the footer's two buttons in a row).
        minWidth: 0,
        background:
          'radial-gradient(ellipse 60% 60% at 100% 0%, rgba(197,52,0,0.08), transparent 55%), ' +
          'linear-gradient(180deg, rgba(20,20,20,0.85) 0%, rgba(10,10,10,0.9) 100%)',
        border: '1px solid var(--line)',
        borderRadius: 5,
        display: 'flex', flexDirection: 'column',
        cursor: 'pointer',
        transition: 'transform .25s ease-out, box-shadow .25s ease-out',
        transform: hover ? 'translateY(-3px)' : 'none',
        boxShadow: hover ? '0 22px 60px -28px rgba(197,52,0,0.55)' : 'none',
        // No minHeight here — the image-header sets its own minHeight
        // and the flex:1 spacer below handles cross-card height parity
        // in the grid. A forced minHeight here would leave a visible
        // empty band between the header and the footer.
      }}
    >
      {/* Bridge brand image as background with a darkening gradient
          overlay for contrast. Same pattern as the wallet card —
          gradient is FIRST in the shorthand so it paints LAST (on
          top of the image). Falls back to the original tinted dark
          surface when headerImage is missing. */}
      <div className="la-product-card__header" style={{
        position: 'relative',
        padding: '18px 20px 20px',
        minHeight: 160,
        borderBottom: '1px solid var(--line)',
        background: bridge.headerImage
          ? `linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.78) 100%), url(${bridge.headerImage}) center/cover no-repeat #0a0a0a`
          : 'rgba(0,0,0,0.18)',
        display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 14,
        minWidth: 0,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div style={{
            width: 52, height: 52, flexShrink: 0,
            background: 'var(--bg)',
            border: '1px solid var(--line)',
            borderRadius: 4,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            overflow: 'hidden',
          }}>
            <img
              src={bridge.logo}
              alt={`${bridge.name} logo`}
              onError={(e) => { e.currentTarget.style.display = 'none' }}
              style={{ width: '78%', height: '78%', objectFit: 'contain' }}
            />
          </div>
          <div className="la-product-card__name-block">
            <div style={{
              fontFamily: 'var(--display)', fontWeight: 800,
              fontSize: 'clamp(15px,1.5vw,20px)', lineHeight: 1.0,
              letterSpacing: '-0.02em', textTransform: 'uppercase',
              color: 'var(--text)',
            }}>{bridge.name}</div>
            {/* Chip line — hidden on desktop, surfaced under the name on
                mobile (controlled by CSS in the <style> block at the end
                of LiveApps). Matches the wallet card's category line. */}
            <div className="la-product-card__category" style={{
              display: 'none',
              marginTop: 6,
              fontFamily: 'var(--mono)', fontSize: 10.5,
              letterSpacing: '0.18em', textTransform: 'uppercase',
              color: 'var(--orange)',
            }}>
              {bridge.chips?.map((c) => `[ ${c} ]`).join(' ')}
            </div>
          </div>
        </div>
      </div>

      {/* Body — description hidden on desktop (the card here is header +
          footer only for visual rhythm in the grid) and shown on mobile
          via CSS to mirror the wallet card's layout. */}
      <div className="la-product-card__body" style={{
        display: 'none',
        padding: '14px 20px 14px',
      }}>
        <p style={{
          margin: 0, fontFamily: 'var(--mono)', fontSize: 12,
          lineHeight: 1.7, letterSpacing: '0.04em', color: 'var(--dim)',
        }}>{bridge.description}</p>
      </div>

      {/* Spacer keeps footer pinned to the bottom of the card on desktop
          so heights stay consistent across the grid. Drops to no-op on
          mobile because the card becomes a CSS grid (children placed by
          named area, no flex spacer needed). */}
      <div className="la-product-card__spacer" style={{ flex: 1 }} />

      {/* Footer can wrap to a second row at narrow card widths so the
          LAUNCH button never gets pushed past the right edge. The two
          inner groups carry flexShrink: 0 + their own min-width: 0 so
          they preserve their natural width when they fit, and wrap as
          a unit when they don't. */}
      <div className="la-card-footer" style={{
        padding: '12px 16px', borderTop: '1px solid var(--line)',
        background: 'rgba(0,0,0,0.18)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 10, flexWrap: 'wrap', rowGap: 10,
      }}>
        <div className="la-card-footer__socials" style={{
          display: 'flex', alignItems: 'center', gap: 2,
          flexShrink: 0, minWidth: 0,
        }}>
          {bridge.twitter && <SocialLink href={bridge.twitter}><Icon.x /></SocialLink>}
          {bridge.website && <SocialLink href={bridge.website}><Icon.globe /></SocialLink>}
        </div>
        <div className="la-card-footer__actions" style={{
          display: 'flex', gap: 8,
          flexShrink: 0, minWidth: 0, justifyContent: 'flex-end',
        }}>
          <GhostButton onClick={() => onPreview(bridge)}>Preview</GhostButton>
          {bridge.url && <GhostButton href={bridge.url}>Launch app</GhostButton>}
        </div>
      </div>
    </div>
  )
}

function BridgeSlidePanel({ bridge, onClose }) {
  const [closing, setClosing] = useState(false)
  const requestClose = () => { if (!closing) setClosing(true) }
  const sheet = useBottomSheetDrag(requestClose, closing)
  const onAnimationEnd = (e) => {
    if (closing && (e.animationName === 'la-slide-out' || e.animationName === 'la-sheet-down')) onClose()
  }
  const onTransitionEnd = (e) => {
    if (closing && e.propertyName === 'transform' && sheet.dragY > 0) onClose()
  }

  useEffect(() => {
    const prev = document.body.style.overflow
    document.body.style.overflow = 'hidden'
    return () => { document.body.style.overflow = prev }
  }, [])

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') requestClose() }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [closing])

  return (
    <>
      <div onClick={requestClose} style={{
        position: 'fixed', inset: 0, zIndex: 90,
        background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
        animation: closing ? 'la-fade-out .26s ease-in forwards' : 'la-fade-in .22s ease-out',
      }} />
      <aside
        className="la-side-panel"
        data-closing={closing ? 'true' : undefined}
        role="dialog" aria-label={`${bridge.name} details`}
        onAnimationEnd={onAnimationEnd}
        onTransitionEnd={onTransitionEnd}
        {...sheet.handlers}
        style={{
          position: 'fixed', top: 72, right: 0, bottom: 0, zIndex: 91,
          width: 'min(560px, 92vw)',
          background: 'var(--bg)',
          border: '1px solid var(--line)', borderRight: 'none',
          borderTopLeftRadius: 8,
          color: 'var(--text)', overflowY: 'auto',
          boxShadow: '0 0 60px rgba(0,0,0,0.45)',
          animation: closing
            ? 'la-slide-out .28s cubic-bezier(.5, 0, .75, 0) forwards'
            : 'la-slide-in .32s cubic-bezier(.2, .7, .2, 1)',
          ...sheet.style,
        }}
      >
        <div className="la-sheet-handle" aria-hidden="true" />
        <button onClick={requestClose} aria-label="Close panel" style={{
          position: 'absolute', top: 16, right: 16, zIndex: 2,
          background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(6px)',
          WebkitBackdropFilter: 'blur(6px)',
          border: '1px solid rgba(255,255,255,0.12)',
          color: 'var(--text)', borderRadius: 2, padding: '0 12px', height: 30,
          fontFamily: 'var(--mono)', fontSize: 11, cursor: 'pointer',
          letterSpacing: '0.08em', textTransform: 'uppercase',
        }}>Close ✕</button>

        <div style={{
          position: 'relative', zIndex: 0,
          width: '100%',
          height: 'clamp(130px, 22vw, 180px)',
          background: bridge.headerImage
            ? `#0a0a0a url(${bridge.headerImage}) center/cover no-repeat`
            : 'linear-gradient(135deg, var(--bg-1), var(--bg-2))',
          borderBottom: '1px solid var(--line)',
          borderTopLeftRadius: 8,
        }} />

        <div style={{ padding: '0 clamp(28px,4vw,44px) clamp(28px,4vw,44px)', position: 'relative' }}>
          <div style={{
            position: 'relative', zIndex: 2,
            width: 144, height: 144, marginTop: -72,
            border: '4px solid var(--bg)', borderRadius: 8,
            background: 'var(--bg-1)', overflow: 'hidden',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: '0 12px 32px rgba(0,0,0,0.5)',
          }}>
            <img
              src={bridge.logo}
              alt={`${bridge.name} logo`}
              onError={(e) => { e.currentTarget.style.display = 'none' }}
              style={{ width: '74%', height: '74%', objectFit: 'contain' }}
            />
          </div>

          <div className="bridge-chip-row bridge-chip-row--lg" style={{ marginTop: 18 }}>
            {bridge.chips?.map((c) => (
              <span key={c} className="bridge-chip bridge-chip--lg">{c}</span>
            ))}
          </div>

          <div style={{
            marginTop: 12, fontFamily: 'var(--display)', fontWeight: 800,
            fontSize: 'clamp(28px,3.4vw,44px)', letterSpacing: '-0.025em',
            lineHeight: 1.0, textTransform: 'uppercase',
          }}>{bridge.name}</div>

          <p style={{
            marginTop: 22, fontFamily: 'var(--mono)', fontSize: 13,
            lineHeight: 1.7, letterSpacing: '0.04em', color: 'var(--text)',
          }}>{bridge.what}</p>

          <div style={{
            display: 'flex', alignItems: 'center',
            gap: 10, marginTop: 32, flexWrap: 'wrap',
            justifyContent: 'space-between',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
              {bridge.website && bridge.website !== bridge.url && (
                <SocialLink href={bridge.website}><Icon.globe size={20} /></SocialLink>
              )}
              {bridge.twitter && (
                <SocialLink href={bridge.twitter}><Icon.x size={20} /></SocialLink>
              )}
            </div>
            {bridge.url && <GhostButton href={bridge.url}>Launch app</GhostButton>}
          </div>
        </div>
      </aside>
    </>
  )
}

/* ── Apps (tab 04) UI ─────────────────────────────────────────────
 * Same card + slide-panel pattern as bridges. Chip row carries the
 * app's category (DEX / Lending / Staking / Yield). Slide panel adds
 * Publicly / Privately bullet lists so users see what the app does in
 * each mode. */

function AppCard({ app, onPreview }) {
  const [hover, setHover] = useState(false)
  return (
    <div
      className="la-product-card"
      onClick={() => onPreview(app)}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPreview(app) } }}
      style={{
        position: 'relative', overflow: 'hidden',
        // isolation:isolate creates a new stacking context for this
        // card. Without it, the "Coming soon" overlay's z-index would
        // compete globally with the AppRouteSelect dropdown (which is
        // z-index:5) and end up painting on top of the open dropdown.
        // Isolated, the overlay can only stack within this card.
        isolation: 'isolate',
        minWidth: 0,
        background:
          'radial-gradient(ellipse 60% 60% at 100% 0%, rgba(197,52,0,0.08), transparent 55%), ' +
          'linear-gradient(180deg, rgba(20,20,20,0.85) 0%, rgba(10,10,10,0.9) 100%)',
        border: '1px solid var(--line)',
        borderRadius: 5,
        display: 'flex', flexDirection: 'column',
        cursor: 'pointer',
        transition: 'transform .25s ease-out, box-shadow .25s ease-out',
        transform: hover ? 'translateY(-3px)' : 'none',
        boxShadow: hover ? '0 22px 60px -28px rgba(197,52,0,0.55)' : 'none',
        // No minHeight here — the image-header sets its own minHeight
        // and the flex:1 spacer below handles cross-card height parity
        // in the grid.
      }}
    >
      {/* "Coming soon" veil. Greys the card without hiding the brand
          image, signals "not live yet". pointer-events:none lets the
          underlying card stay clickable so users can still open the
          slide-in preview to read about the app. The SOON pill in the
          top-right is the explicit caption — readable on any header. */}
      <div aria-hidden="true" style={{
        position: 'absolute', inset: 0, zIndex: 5,
        background: 'rgba(20,20,20,0.46)',
        pointerEvents: 'none',
      }} />
      <div aria-hidden="true" style={{
        position: 'absolute', top: 10, right: 10, zIndex: 6,
        padding: '4px 9px', borderRadius: 999,
        background: 'rgba(10,10,10,0.78)',
        border: '1px solid rgba(197,52,0,0.45)',
        fontFamily: 'var(--mono)', fontSize: 9.5,
        letterSpacing: '0.22em', textTransform: 'uppercase',
        color: 'var(--orange)',
        pointerEvents: 'none',
      }}>Coming soon</div>

      {/* App brand image as background with a darkening gradient
          overlay for contrast — same pattern as the bridge card.
          Chip row pins to the top, logo + name pin to the bottom. */}
      <div className="la-product-card__header" style={{
        position: 'relative',
        padding: '18px 20px 20px',
        minHeight: 160,
        borderBottom: '1px solid var(--line)',
        background: app.headerImage
          ? `linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.78) 100%), url(${app.headerImage}) center/cover no-repeat #0a0a0a`
          : 'rgba(0,0,0,0.18)',
        display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 14,
        minWidth: 0,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div style={{
            width: 52, height: 52, flexShrink: 0,
            background: 'var(--bg)',
            border: '1px solid var(--line)',
            borderRadius: 4,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            overflow: 'hidden',
          }}>
            <img
              src={app.logo}
              alt={`${app.name} logo`}
              onError={(e) => { e.currentTarget.style.display = 'none' }}
              style={{ width: '78%', height: '78%', objectFit: 'contain' }}
            />
          </div>
          <div className="la-product-card__name-block">
            <div style={{
              fontFamily: 'var(--display)', fontWeight: 800,
              fontSize: 'clamp(15px,1.5vw,20px)', lineHeight: 1.0,
              letterSpacing: '-0.02em', textTransform: 'uppercase',
              color: 'var(--text)',
            }}>{app.name}</div>
            {/* Chip line — hidden on desktop, surfaced on mobile under
                the name to mirror the wallet/bridge card layout. */}
            <div className="la-product-card__category" style={{
              display: 'none',
              marginTop: 6,
              fontFamily: 'var(--mono)', fontSize: 10.5,
              letterSpacing: '0.18em', textTransform: 'uppercase',
              color: 'var(--orange)',
            }}>
              {app.chips?.map((c) => `[ ${c} ]`).join(' ')}
            </div>
          </div>
        </div>
      </div>

      {/* Body — hidden on desktop, shown on mobile by CSS so all three
          card types (wallet, bridge, app) render the same way on small
          viewports. */}
      <div className="la-product-card__body" style={{
        display: 'none',
        padding: '14px 20px 14px',
      }}>
        <p style={{
          margin: 0, fontFamily: 'var(--mono)', fontSize: 12,
          lineHeight: 1.7, letterSpacing: '0.04em', color: 'var(--dim)',
        }}>{app.description}</p>
      </div>

      <div className="la-card-footer" style={{
        padding: '12px 16px', borderTop: '1px solid var(--line)',
        background: 'rgba(0,0,0,0.18)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 10, flexWrap: 'wrap', rowGap: 10,
      }}>
        <div className="la-card-footer__socials" style={{
          display: 'flex', alignItems: 'center', gap: 2,
          flexShrink: 0, minWidth: 0,
        }}>
          {app.twitter && <SocialLink href={app.twitter}><Icon.x /></SocialLink>}
          {app.discord && <SocialLink href={app.discord}><Icon.discord /></SocialLink>}
          {app.website && <SocialLink href={app.website}><Icon.globe /></SocialLink>}
        </div>
        <div className="la-card-footer__actions" style={{
          display: 'flex', gap: 8,
          flexShrink: 0, minWidth: 0, justifyContent: 'flex-end',
        }}>
          <GhostButton onClick={() => onPreview(app)}>Preview</GhostButton>
          {app.url && <GhostButton href={app.url}>Launch app</GhostButton>}
        </div>
      </div>
    </div>
  )
}

/* Placeholder card for apps not yet live. Non-interactive (no onClick,
 * no role, not focusable), dimmed, dashed border so the grid keeps its
 * 3-up rhythm and signals that more apps are landing. */
function ComingSoonAppCard() {
  return (
    <div
      aria-disabled="true"
      style={{
        position: 'relative', overflow: 'hidden',
        minWidth: 0,
        background:
          'linear-gradient(180deg, rgba(20,20,20,0.55) 0%, rgba(10,10,10,0.65) 100%)',
        border: '1px dashed var(--line)',
        borderRadius: 5,
        display: 'flex', flexDirection: 'column',
        cursor: 'not-allowed',
        opacity: 0.55,
        minHeight: 170,
        userSelect: 'none',
      }}
    >
      <div style={{
        padding: '18px 20px 20px',
        borderBottom: '1px dashed var(--line)',
        background: 'rgba(0,0,0,0.12)',
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between', gap: 14,
        minWidth: 0,
      }}>
        <div className="bridge-chip-row">
          <span className="bridge-chip">Soon</span>
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 14 }}>
          <div style={{
            fontFamily: 'var(--display)', fontWeight: 800,
            fontSize: 'clamp(15px,1.5vw,20px)', lineHeight: 1.0,
            letterSpacing: '-0.02em', textTransform: 'uppercase',
            color: 'var(--dim)',
          }}>Coming soon</div>
          <div style={{
            width: 52, height: 52, flexShrink: 0,
            background: 'var(--bg)',
            border: '1px dashed var(--line)',
            borderRadius: 4,
          }} />
        </div>
      </div>

      <div style={{ flex: 1 }} />

      <div style={{
        padding: '12px 16px', borderTop: '1px dashed var(--line)',
        background: 'rgba(0,0,0,0.12)',
        minHeight: 52,
      }} />
    </div>
  )
}

function AppSlidePanel({ app, onClose }) {
  const [closing, setClosing] = useState(false)
  const requestClose = () => { if (!closing) setClosing(true) }
  const sheet = useBottomSheetDrag(requestClose, closing)
  const onAnimationEnd = (e) => {
    if (closing && (e.animationName === 'la-slide-out' || e.animationName === 'la-sheet-down')) onClose()
  }
  const onTransitionEnd = (e) => {
    if (closing && e.propertyName === 'transform' && sheet.dragY > 0) onClose()
  }

  useEffect(() => {
    const prev = document.body.style.overflow
    document.body.style.overflow = 'hidden'
    return () => { document.body.style.overflow = prev }
  }, [])

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') requestClose() }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [closing])

  return (
    <>
      <div onClick={requestClose} style={{
        position: 'fixed', inset: 0, zIndex: 90,
        background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
        animation: closing ? 'la-fade-out .26s ease-in forwards' : 'la-fade-in .22s ease-out',
      }} />
      <aside
        className="la-side-panel"
        data-closing={closing ? 'true' : undefined}
        role="dialog" aria-label={`${app.name} details`}
        onAnimationEnd={onAnimationEnd}
        onTransitionEnd={onTransitionEnd}
        {...sheet.handlers}
        style={{
          position: 'fixed', top: 72, right: 0, bottom: 0, zIndex: 91,
          width: 'min(560px, 92vw)',
          background: 'var(--bg)',
          border: '1px solid var(--line)', borderRight: 'none',
          borderTopLeftRadius: 8,
          color: 'var(--text)', overflowY: 'auto',
          boxShadow: '0 0 60px rgba(0,0,0,0.45)',
          animation: closing
            ? 'la-slide-out .28s cubic-bezier(.5, 0, .75, 0) forwards'
            : 'la-slide-in .32s cubic-bezier(.2, .7, .2, 1)',
          ...sheet.style,
        }}
      >
        <div className="la-sheet-handle" aria-hidden="true" />
        <button onClick={requestClose} aria-label="Close panel" style={{
          position: 'absolute', top: 16, right: 16, zIndex: 2,
          background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(6px)',
          WebkitBackdropFilter: 'blur(6px)',
          border: '1px solid rgba(255,255,255,0.12)',
          color: 'var(--text)', borderRadius: 2, padding: '0 12px', height: 30,
          fontFamily: 'var(--mono)', fontSize: 11, cursor: 'pointer',
          letterSpacing: '0.08em', textTransform: 'uppercase',
        }}>Close ✕</button>

        <div style={{
          position: 'relative', zIndex: 0,
          width: '100%',
          height: 'clamp(130px, 22vw, 180px)',
          background: app.headerImage
            ? `#0a0a0a url(${app.headerImage}) center/cover no-repeat`
            : 'linear-gradient(135deg, var(--bg-1), var(--bg-2))',
          borderBottom: '1px solid var(--line)',
          borderTopLeftRadius: 8,
        }} />

        <div style={{ padding: '0 clamp(28px,4vw,44px) clamp(28px,4vw,44px)', position: 'relative' }}>
          <div style={{
            position: 'relative', zIndex: 2,
            width: 144, height: 144, marginTop: -72,
            border: '4px solid var(--bg)', borderRadius: 8,
            background: 'var(--bg-1)', overflow: 'hidden',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: '0 12px 32px rgba(0,0,0,0.5)',
          }}>
            <img
              src={app.logo}
              alt={`${app.name} logo`}
              onError={(e) => { e.currentTarget.style.display = 'none' }}
              style={{ width: '74%', height: '74%', objectFit: 'contain' }}
            />
          </div>

          <div className="bridge-chip-row bridge-chip-row--lg" style={{ marginTop: 18 }}>
            {app.chips?.map((c) => (
              <span key={c} className="bridge-chip bridge-chip--lg">{c}</span>
            ))}
          </div>

          <div style={{
            marginTop: 12, fontFamily: 'var(--display)', fontWeight: 800,
            fontSize: 'clamp(28px,3.4vw,44px)', letterSpacing: '-0.025em',
            lineHeight: 1.0, textTransform: 'uppercase',
          }}>{app.name}</div>

          <p style={{
            marginTop: 22, fontFamily: 'var(--mono)', fontSize: 13,
            lineHeight: 1.7, letterSpacing: '0.04em', color: 'var(--text)',
          }}>{app.what}</p>

          {app.publicly?.length > 0 && (
            <div style={{ marginTop: 26 }}>
              <div style={{
                fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.14em',
                textTransform: 'uppercase', color: 'var(--dim)', marginBottom: 10,
              }}>Publicly</div>
              <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
                {app.publicly.map((line, i) => (
                  <li key={i} style={{
                    display: 'flex', gap: 10,
                    fontFamily: 'var(--mono)', fontSize: 12.5,
                    lineHeight: 1.6, letterSpacing: '0.04em', color: 'var(--text)',
                  }}>
                    <span>{line}</span>
                  </li>
                ))}
              </ul>
            </div>
          )}

          {app.privately?.length > 0 && (
            <div style={{ marginTop: 26 }}>
              <div style={{
                display: 'flex', alignItems: 'center', gap: 8,
                marginBottom: 10,
                fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.14em',
                textTransform: 'uppercase', color: 'var(--green)',
              }}>
                <span>Privately via strk20</span>
                {/* Anonymizer contracts for these apps aren't shipped
                    yet; this pill makes the dependency visible to
                    users reading the bullet list below. */}
                <span style={{
                  padding: '2px 8px',
                  border: '1px solid rgba(197,52,0,0.45)',
                  borderRadius: 999,
                  fontSize: 9.5, letterSpacing: '0.2em',
                  color: 'var(--orange)',
                  background: 'rgba(197,52,0,0.08)',
                }}>Coming soon</span>
              </div>
              <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
                {app.privately.map((line, i) => (
                  <li key={i} style={{
                    display: 'flex', gap: 10,
                    fontFamily: 'var(--mono)', fontSize: 12.5,
                    lineHeight: 1.6, letterSpacing: '0.04em', color: 'var(--text)',
                  }}>
                    <span>{line}</span>
                  </li>
                ))}
              </ul>
            </div>
          )}

          <div style={{
            display: 'flex', alignItems: 'center',
            gap: 10, marginTop: 32, flexWrap: 'wrap',
            justifyContent: 'space-between',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
              {app.website && app.website !== app.url && (
                <SocialLink href={app.website}><Icon.globe size={20} /></SocialLink>
              )}
              {app.twitter && (
                <SocialLink href={app.twitter}><Icon.x size={20} /></SocialLink>
              )}
            </div>
            {app.url && <GhostButton href={app.url}>Launch app</GhostButton>}
          </div>
        </div>
      </aside>
    </>
  )
}

/* ── Shield (tab 03) module ────────────────────────────────────────
 * Single-row token select (amount + AVNU token dropdown), SHIELD /
 * UNSHIELD segmented toggle below it, then an OPEN WALLET action
 * button that is disabled until the Ready / Xverse wallet specs ship.
 *
 * Token list comes from AVNU's public tokens endpoint; a small built-
 * in fallback covers the case where the fetch fails (offline, CORS,
 * API down) so the dropdown is never empty. */
const AVNU_TOKENS_URL = 'https://starknet.api.avnu.fi/v1/starknet/tokens'

const FALLBACK_TOKENS = [
  { symbol: 'ETH',  name: 'Ether',           address: '0x049d36570d4e46f48e99674bd3fcc8463f400b38e8c1932d4af13e85e8c6b1c8', logoUri: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png',         decimals: 18 },
  { symbol: 'STRK', name: 'Starknet',        address: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', logoUri: 'https://assets.coingecko.com/coins/images/26433/large/starknet.png',       decimals: 18 },
  { symbol: 'USDC', name: 'USD Coin',        address: '0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8', logoUri: 'https://assets.coingecko.com/coins/images/6319/large/usdc.png',            decimals: 6 },
  { symbol: 'USDT', name: 'Tether',          address: '0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8', logoUri: 'https://assets.coingecko.com/coins/images/325/large/Tether.png',           decimals: 6 },
  { symbol: 'WBTC', name: 'Wrapped Bitcoin', address: '0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac', logoUri: 'https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png', decimals: 8 },
  { symbol: 'DAI',  name: 'Dai',             address: '0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3', logoUri: 'https://assets.coingecko.com/coins/images/9956/large/Badge_Dai.png',         decimals: 18 },
]

/* Symbols of tokens the privacy pool currently accepts. The pool
 * contract does NOT expose a get_supported_tokens() view — the live
 * truth would have to come from indexing Deposit(user, token, amount)
 * event history. Until a server-side indexer is wired in here, this
 * curated list mirrors the SDK demo's mainnet token config. Icons +
 * full names get resolved against AVNU's public token list (fetched
 * once in ShieldModule and passed down). */
const POOL_TOKEN_SYMBOLS = ['ETH', 'STRK', 'USDC', 'USDT', 'WBTC', 'DAI']

/* Hardcoded Private DeFi entries surfaced on the Shield tab. Each row
 * maps to an app already covered on the Apps (04) tab — VESU vaults,
 * ENDUR staking, AVNU swaps, TROVES vaults. The `url` opens the app's
 * external surface (Apps-tab parity); the icon set lives in
 * /public/dashboard-assets/logos/. */
const PRIVATE_DEFI = [
  { id: 'vesu-strk',   app: 'Vesu',   title: 'Top STRK supply vault',   logo: '/dashboard-assets/logos/vesu.png',   url: 'https://vesu.xyz/pro/earn' },
  { id: 'vesu-usdc',   app: 'Vesu',   title: 'Top USDC supply vault',   logo: '/dashboard-assets/logos/vesu.png',   url: 'https://vesu.xyz/pro/earn' },
  { id: 'vesu-eth',    app: 'Vesu',   title: 'Top ETH supply vault',    logo: '/dashboard-assets/logos/vesu.png',   url: 'https://vesu.xyz/pro/earn' },
  { id: 'endur-strk',  app: 'Endur',  title: 'Stake STRK → xSTRK',      logo: '/dashboard-assets/logos/endur.png',  url: 'https://app.endur.fi/' },
  { id: 'endur-btc',   app: 'Endur',  title: 'Stake BTC (strkBTC / WBTC)', logo: '/dashboard-assets/logos/endur.png', url: 'https://app.endur.fi/' },
  { id: 'avnu-swap',   app: 'AVNU',   title: 'Swap any token, privately', logo: '/dashboard-assets/logos/avnu.png',  url: 'https://app.avnu.fi/' },
  { id: 'troves-vault',app: 'Troves', title: 'Automated yield vaults',  logo: '/dashboard-assets/logos/troves.png', url: 'https://app.troves.fi/' },
]

function SupportedTokensPanel({ tokens }) {
  // tokens prop carries the AVNU-fetched list (or FALLBACK_TOKENS).
  // Pick the curated pool-supported subset, keep AVNU's order. If a
  // symbol isn't present in AVNU's response, skip it rather than
  // rendering an empty row.
  const bySymbol = new Map()
  for (const t of tokens) {
    const sym = (t.symbol || '').toUpperCase()
    if (!sym || bySymbol.has(sym)) continue
    bySymbol.set(sym, t)
  }
  const pool = POOL_TOKEN_SYMBOLS
    .map((s) => bySymbol.get(s))
    .filter(Boolean)

  return (
    <div className="shield-side-panel" style={{
      background: 'rgba(0,0,0,0.30)',
      border: '1px solid var(--line)',
      borderRadius: 4,
      padding: '16px 0',
      minWidth: 0,
    }}>
      <div style={{
        padding: '0 18px 12px',
        borderBottom: '1px solid var(--line)',
        fontFamily: 'var(--mono)', fontSize: 10.5,
        letterSpacing: '0.2em', textTransform: 'uppercase',
        color: 'var(--dim)',
      }}>
        Supported tokens
      </div>
      <ul style={{
        listStyle: 'none', padding: 0, margin: 0,
        maxHeight: 340, overflowY: 'auto',
      }}>
        {pool.map((t) => (
          <li key={t.address || t.symbol} style={{
            display: 'flex', alignItems: 'center', gap: 12,
            padding: '11px 18px',
            borderBottom: '1px solid rgba(255,255,255,0.04)',
          }}>
            {t.logoUri ? (
              <img src={t.logoUri} alt=""
                   onError={(e) => { e.currentTarget.style.visibility = 'hidden' }}
                   style={{ width: 26, height: 26, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 }} />
            ) : (
              <div style={{ width: 26, height: 26, borderRadius: '50%', background: 'var(--bg-1)', flexShrink: 0 }} />
            )}
            <span style={{
              fontFamily: 'var(--mono)', fontSize: 12,
              letterSpacing: '0.1em', textTransform: 'uppercase',
              fontWeight: 600, color: 'var(--text)',
              flexShrink: 0,
            }}>{t.symbol}</span>
            <span style={{
              fontFamily: 'var(--mono)', fontSize: 11,
              letterSpacing: '0.06em',
              color: 'var(--dim)',
              overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
            }}>{t.name}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

function PrivateDefiPanel() {
  return (
    <div className="shield-side-panel" style={{
      background: 'rgba(0,0,0,0.30)',
      border: '1px solid var(--line)',
      borderRadius: 4,
      padding: '16px 0',
      minWidth: 0,
    }}>
      <div style={{
        padding: '0 18px 12px',
        borderBottom: '1px solid var(--line)',
        fontFamily: 'var(--mono)', fontSize: 10.5,
        letterSpacing: '0.2em', textTransform: 'uppercase',
        color: 'var(--dim)',
      }}>
        Private DeFi
      </div>
      <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
        {PRIVATE_DEFI.map((d) => (
          <li key={d.id}>
            <a
              href={d.url} target="_blank" rel="noopener noreferrer"
              style={{
                display: 'flex', alignItems: 'center', gap: 12,
                padding: '11px 18px',
                borderBottom: '1px solid rgba(255,255,255,0.04)',
                textDecoration: 'none', color: 'inherit',
                transition: 'background .15s',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(197,52,0,0.06)' }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }}
            >
              <div style={{
                width: 28, height: 28, flexShrink: 0,
                background: 'var(--bg-1)',
                border: '1px solid var(--line)', borderRadius: 4,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                overflow: 'hidden',
              }}>
                <img src={d.logo} alt={`${d.app} logo`}
                     onError={(e) => { e.currentTarget.style.visibility = 'hidden' }}
                     style={{ width: '78%', height: '78%', objectFit: 'contain' }} />
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', minWidth: 0, gap: 2 }}>
                <span style={{
                  fontFamily: 'var(--mono)', fontSize: 10,
                  letterSpacing: '0.18em', textTransform: 'uppercase',
                  color: 'var(--orange)',
                }}>{d.app}</span>
                <span style={{
                  fontFamily: 'var(--mono)', fontSize: 12,
                  letterSpacing: '0.04em',
                  color: 'var(--text)',
                  overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
                }}>{d.title}</span>
              </div>
            </a>
          </li>
        ))}
      </ul>
    </div>
  )
}

function ShieldModule() {
  const [mode, setMode] = useState('shield')      // 'shield' | 'unshield'
  const [amount, setAmount] = useState('')
  const [tokens, setTokens] = useState(FALLBACK_TOKENS)
  const [selectedToken, setSelectedToken] = useState(FALLBACK_TOKENS[0])
  const [dropdownOpen, setDropdownOpen] = useState(false)
  const dropdownRef = useRef(null)

  // Fetch AVNU tokens on mount; fall back to the built-in list on
  // failure so the dropdown is always populated.
  useEffect(() => {
    let alive = true
    fetch(AVNU_TOKENS_URL)
      .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
      .then((data) => {
        if (!alive) return
        const list = Array.isArray(data) ? data : (data.content || data.tokens || [])
        const filtered = list.filter((t) => t && t.symbol)
        if (filtered.length) {
          setTokens(filtered)
          setSelectedToken(
            filtered.find((t) => t.symbol === 'ETH') || filtered[0]
          )
        }
      })
      .catch(() => { /* fallback stays */ })
    return () => { alive = false }
  }, [])

  // Click-outside + Escape close the dropdown.
  useEffect(() => {
    if (!dropdownOpen) return
    const onClick = (e) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
        setDropdownOpen(false)
      }
    }
    const onKey = (e) => { if (e.key === 'Escape') setDropdownOpen(false) }
    document.addEventListener('mousedown', onClick)
    document.addEventListener('keydown', onKey)
    return () => {
      document.removeEventListener('mousedown', onClick)
      document.removeEventListener('keydown', onKey)
    }
  }, [dropdownOpen])

  return (
    <div className="shield-tab-grid" style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
      gap: 'clamp(18px, 2vw, 32px)',
      alignItems: 'start',
      fontFamily: 'var(--mono)',
    }}>
    <div className="shield-module" style={{
      display: 'flex', flexDirection: 'column',
      minWidth: 0,
      // Whole module uses var(--mono) — matches the 'Total value
      // shielded' label on the landing page. All text below is
      // uppercase via textTransform on individual elements.
      fontFamily: 'var(--mono)',
    }}>
      {/* ── Token select box ───────────────────────────────────── */}
      <div style={{
        background: 'rgba(0,0,0,0.30)',
        border: '1px solid var(--line)',
        borderRadius: 4,
        padding: '16px 0 22px',
      }}>
        {/* Wallet integration stripped from this branch — the
            "From: …" label is the only thing this row carries now. */}
        <div style={{
          display: 'flex', alignItems: 'center',
          padding: '0 20px', marginBottom: 14,
        }}>
          <span style={{
            fontFamily: 'var(--mono)', fontSize: 10.5,
            letterSpacing: '0.2em', textTransform: 'uppercase',
            color: 'var(--dim)',
          }}>
            {mode === 'shield' ? 'From: Public balance' : 'From: Shielded balance'}
          </span>
        </div>

        <div style={{ height: 1, background: 'var(--line)', marginBottom: 16 }} />

        {/* Amount input + token select trigger */}
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          gap: 14, padding: '0 20px',
        }}>
          <input
            type="text" inputMode="decimal" placeholder="0.00"
            value={amount}
            onChange={(e) => setAmount(e.target.value.replace(/[^0-9.]/g, ''))}
            style={{
              flex: 1, minWidth: 0,
              background: 'transparent', border: 'none', outline: 'none',
              color: 'var(--text)',
              // Mono throughout the module (per 'Total value shielded'
              // styling on the landing page); the bigger numeric scale
              // gets weight 600 to balance against mono's wider glyphs.
              fontFamily: 'var(--mono)', fontWeight: 600,
              fontSize: 30, letterSpacing: '-0.01em', padding: 0,
            }}
          />
          <div ref={dropdownRef} style={{ position: 'relative', flexShrink: 0 }}>
            <button
              type="button"
              onClick={() => setDropdownOpen((o) => !o)}
              style={{
                display: 'inline-flex', alignItems: 'center', gap: 8,
                background: 'transparent', border: 'none', cursor: 'pointer',
                color: 'var(--text)', fontFamily: 'var(--mono)', fontSize: 13,
                letterSpacing: '0.14em', textTransform: 'uppercase',
                fontWeight: 600, padding: 0,
              }}
            >
              {selectedToken?.logoUri ? (
                <img src={selectedToken.logoUri} alt=""
                     onError={(e) => { e.currentTarget.style.visibility = 'hidden' }}
                     style={{ width: 26, height: 26, borderRadius: '50%', objectFit: 'cover' }} />
              ) : (
                <div style={{ width: 26, height: 26, borderRadius: '50%', background: 'var(--bg-1)' }} />
              )}
              <span>{selectedToken?.symbol || '—'}</span>
              <span style={{ opacity: 0.6, marginLeft: 2 }}>▾</span>
            </button>

            {dropdownOpen && (
              <div style={{
                position: 'absolute', top: 'calc(100% + 10px)', right: 0,
                width: 280, maxHeight: 320, overflowY: 'auto',
                background: 'var(--bg)', border: '1px solid var(--line)',
                borderTop: '2px solid var(--orange)',
                borderRadius: 4, padding: 6, zIndex: 10,
                boxShadow: '0 18px 40px -12px rgba(0,0,0,0.55)',
              }}>
                {tokens.map((t) => (
                  <button
                    key={t.address || t.symbol}
                    type="button"
                    onClick={() => { setSelectedToken(t); setDropdownOpen(false) }}
                    style={{
                      display: 'flex', alignItems: 'center', gap: 10,
                      width: '100%', padding: '8px 10px',
                      background: 'transparent', border: 'none',
                      borderRadius: 3, cursor: 'pointer',
                      color: 'var(--text)', fontFamily: 'var(--mono)', fontSize: 11.5,
                      letterSpacing: '0.1em', textTransform: 'uppercase',
                      textAlign: 'left',
                      transition: 'background .15s',
                    }}
                    onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(197,52,0,0.08)' }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }}
                  >
                    {t.logoUri ? (
                      <img src={t.logoUri} alt=""
                           onError={(e) => { e.currentTarget.style.visibility = 'hidden' }}
                           style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 }} />
                    ) : (
                      <div style={{ width: 22, height: 22, borderRadius: '50%', background: 'var(--bg-1)', flexShrink: 0 }} />
                    )}
                    <span style={{ fontWeight: 600 }}>{t.symbol}</span>
                    <span style={{
                      color: 'var(--dim)', fontSize: 10.5, letterSpacing: '0.08em',
                      overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
                    }}>{t.name}</span>
                  </button>
                ))}
              </div>
            )}
          </div>
        </div>
      </div>

      {/* ── SHIELD / UNSHIELD segmented toggle ──────────────────── */}
      {/* marginTop bumps the gap between the token box and the toggle
          so the two read as distinct groups (input vs. action). */}
      <div style={{
        marginTop: 24,
        display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, padding: 4,
        background: 'rgba(0,0,0,0.30)',
        border: '1px solid var(--line)', borderRadius: 4,
      }}>
        {['shield', 'unshield'].map((m) => (
          <button
            key={m}
            type="button"
            onClick={() => setMode(m)}
            style={{
              padding: '13px 0', background: 'transparent',
              border: mode === m ? '1px solid var(--orange)' : '1px solid transparent',
              color: mode === m ? 'var(--text)' : 'var(--dim)',
              borderRadius: 3, cursor: 'pointer',
              // Same type spec as the landing page "Total value
              // shielded" label — mono, 11.5px, 0.06em tracking,
              // uppercase, no bold even when selected.
              fontFamily: 'var(--mono)', fontSize: 11.5,
              letterSpacing: '0.06em', textTransform: 'uppercase',
              fontWeight: 400,
              transition: 'border-color .18s, color .18s',
            }}
          >{m}</button>
        ))}
      </div>

      {/* ── OPEN WALLET (disabled — Ready/Xverse specs pending) ─── */}
      <button
        type="button" disabled aria-disabled="true"
        title="Wallet integration ships once Ready/Xverse specs are live"
        style={{
          marginTop: 12,
          width: '100%', padding: '16px 0',
          background: 'rgba(197,52,0,0.14)', color: 'var(--text)',
          border: '1px solid rgba(197,52,0,0.32)', borderRadius: 3,
          // Same type spec as the landing page "Total value
          // shielded" label — mono, 11.5px, 0.06em tracking,
          // uppercase, normal weight.
          fontFamily: 'var(--mono)', fontSize: 11.5,
          letterSpacing: '0.06em', textTransform: 'uppercase',
          fontWeight: 400, cursor: 'not-allowed', opacity: 0.55,
        }}
      >
        Open wallet
      </button>
    </div>

      <SupportedTokensPanel tokens={tokens} />
      <PrivateDefiPanel />
    </div>
  )
}

/* Orange RFP-card gradient lifted from the landing-page palette,
   stretched to span the full tab row. The progress fill (below)
   reveals this gradient as the user clicks through tabs. */
const ORANGE_FILL_GRADIENT =
  'radial-gradient(ellipse 95% 120% at 10% 0%, #d6531c 0%, rgba(197,52,0,0) 58%), ' +
  'linear-gradient(128deg, #a02a00 0%, #c53400 62%, #d6531c 120%)'

function TabCell({ tab, isActive, isFilled, onClick, isRightEdge, isBottomRow }) {
  const [hover, setHover] = useState(false)
  // Show the dark-tinted hover background only on cells the progress
  // bar HASN'T already filled — otherwise it muddies the orange.
  const showHoverBg = hover && !isFilled
  // On the mobile horizontal-scroll strip, center the tapped tab in
  // the scroller so the user can immediately tap an adjacent one.
  // scroll-snap handles momentum scroll; this handles the tap path.
  // No-op on desktop where the parent isn't an overflow scroller.
  const handleClick = (e) => {
    e.currentTarget.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
    onClick()
  }
  return (
    <button
      onClick={handleClick}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      aria-label={tab.title}
      style={{
        position: 'relative', zIndex: 1, overflow: 'hidden',
        textAlign: 'left', cursor: 'pointer',
        background: showHoverBg
          ? 'radial-gradient(ellipse 70% 80% at 50% 100%, rgba(197,52,0,0.16), transparent 65%), rgba(20,20,20,0.55)'
          : 'transparent',
        border: 'none',
        borderRight: isRightEdge
          ? 'none'
          : `1px solid ${isFilled ? 'rgba(255,255,255,0.14)' : 'var(--line)'}`,
        borderBottom: isBottomRow
          ? 'none'
          : `1px solid ${isFilled ? 'rgba(255,255,255,0.14)' : 'var(--line)'}`,
        padding: 'clamp(12px,1.4vw,18px) clamp(12px,1.4vw,18px)',
        minHeight: 88,
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
        gap: 'clamp(8px,1.2vh,14px)',
        transition: 'background .22s ease-out, border-color .35s ease',
        color: 'inherit', fontFamily: 'inherit',
      }}
    >
      {/* Number — orange when unfilled, light when the fill has reached
          this cell. Hidden on mobile (icon-only mode). */}
      <div className="la-tab-number" style={{
        fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.14em',
        color: isFilled ? 'rgba(255,253,241,0.85)' : 'var(--orange)',
        transition: 'color .35s ease',
      }}>{tab.number}</div>

      {/* Title — uppercase display. Hidden on mobile, replaced with the
          .la-tab-icon SVG below. */}
      <h3
        className="la-tab-title"
        style={{
          margin: 0, padding: 0,
          fontFamily: 'var(--display)', fontWeight: 800,
          fontSize: 'clamp(12px,1.05vw,15px)', lineHeight: 1.05,
          letterSpacing: '-0.015em', textTransform: 'uppercase',
          color: 'var(--text)',
          textWrap: 'balance',
        }}
      >
        {tab.title}
      </h3>

      {/* Icon — hidden on desktop, shown on mobile in place of the
          number + title to keep the tab strip readable on narrow
          screens. Color inherits from currentColor on the parent button
          so the active / filled state automatically tints the SVG. */}
      <div className="la-tab-icon" aria-hidden="true" style={{
        display: 'none',
        alignItems: 'center', justifyContent: 'center',
        color: isFilled ? 'rgba(255,253,241,0.95)' : 'var(--orange)',
        transition: 'color .35s ease',
      }}>{tab.icon}</div>
    </button>
  )
}

function SlideDownPanel({
  activeTab, tab, onPreviewWallet,
  onPreviewBridge, selectedRoute, onSelectRoute,
  onPreviewApp, selectedAppRoute, onSelectAppRoute,
}) {
  const open = activeTab !== null
  // Once the slide-down animation finishes, unclip the inner wrapper
  // so descendants that overflow vertically (like the BridgeRoute
  // dropdown menu when only a small card grid is shown) can extend
  // beyond the panel content area. During open/close transitions we
  // need overflow:hidden so the grid-template-rows 0fr→1fr animation
  // can clip the content; after the animation lands, overflow:visible
  // is safe and required for the dropdown to fully show all options.
  const [unclip, setUnclip] = useState(false)
  useEffect(() => {
    if (open) {
      const t = setTimeout(() => setUnclip(true), 420)
      return () => clearTimeout(t)
    }
    setUnclip(false)
  }, [open])

  // Tab 01 (Get Wallet) renders the wallet card grid; tab 02 (Bridge)
  // renders the bridge grid + route dropdown. All other tabs keep the
  // placeholder header for now until their content lands.
  const isGetWallet = tab?.id === 0
  const isBridge = tab?.id === 1
  const isShield = tab?.id === 2
  const isApps = tab?.id === 3
  const isShieldFunds = tab?.id === 4
  const isShieldedMode = tab?.id === 5
  // Filter bridges by the active route, if any. No route → show all.
  let visibleBridges = []
  if (isBridge) {
    if (selectedRoute) {
      const route = BRIDGE_ROUTES.find((r) => r.id === selectedRoute)
      visibleBridges = route
        ? BRIDGES.filter((b) => route.bridgeIds.includes(b.id))
        : BRIDGES
    } else {
      visibleBridges = BRIDGES
    }
  }
  // Same pattern for apps: filter APPS by active app-route, if any.
  let visibleApps = []
  if (isApps) {
    if (selectedAppRoute) {
      const route = APP_ROUTES.find((r) => r.id === selectedAppRoute)
      visibleApps = route
        ? APPS.filter((a) => route.appIds.includes(a.id))
        : APPS
    } else {
      visibleApps = APPS
    }
  }
  return (
    <div style={{
      // grid-template-rows 0fr -> 1fr is the modern way to height-animate
      // an auto-sized container without measuring the content.
      marginTop: open ? 'clamp(18px,2.6vh,28px)' : 0,
      display: 'grid',
      gridTemplateRows: open ? '1fr' : '0fr',
      transition: 'grid-template-rows .38s cubic-bezier(.2,.7,.2,1), margin-top .38s ease-out',
    }}>
      <div style={{ overflow: unclip ? 'visible' : 'hidden', minHeight: 0 }}>
        <div className="la-section-panel" style={{
          padding: 'clamp(28px,4vw,52px)',
          background:
            'radial-gradient(ellipse 60% 70% at 100% 0%, rgba(197,52,0,0.08), transparent 55%), ' +
            'linear-gradient(180deg, rgba(20,20,20,0.85) 0%, rgba(10,10,10,0.9) 100%)',
          border: '1px solid var(--line)',
          borderTop: '2px solid var(--orange)',
          borderRadius: 5,
          minHeight: 180,
          opacity: open ? 1 : 0,
          transition: 'opacity .26s ease-out',
          transitionDelay: open ? '.12s' : '0s',
        }}>
          {/* Inner wrapper keyed by tab id so React remounts it on each
              tab switch, re-triggering the la-tab-swap animation. The
              outer panel keeps its open/close grid-row + opacity
              transitions — this just layers a per-swap fade-rise on
              top of them. */}
          <div key={tab?.id ?? 'none'} className="la-tab-content">
          <div style={{
            fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.14em',
            color: 'var(--orange)',
          }}>
            {tab?.number ?? '01'}
          </div>
          <h3 style={{
            margin: '14px 0 18px', padding: 0,
            fontFamily: 'var(--display)', fontWeight: 800,
            fontSize: 'clamp(28px,3.2vw,44px)', lineHeight: 1.0,
            letterSpacing: '-0.025em', textTransform: 'uppercase',
            color: 'var(--text)',
          }}>
            {tab?.title ?? 'XYZ'}
          </h3>

          {isGetWallet && (
            <div className="wallets-grid" style={{
              display: 'grid',
              gridTemplateColumns: 'repeat(2, 1fr)',
              gap: 14,
            }}>
              {WALLETS.map((w) => (
                <WalletCard key={w.id} wallet={w} onPreview={onPreviewWallet} />
              ))}
            </div>
          )}

          {isBridge && (
            <>
              <BridgeRouteSelect selectedRoute={selectedRoute} onSelect={onSelectRoute} />
              <div className="bridges-grid" style={{
                marginTop: 24,
                display: 'grid',
                gridTemplateColumns: 'repeat(3, 1fr)',
                gap: 14,
              }}>
                {visibleBridges.map((b) => (
                  <BridgeCard key={b.id} bridge={b} onPreview={onPreviewBridge} />
                ))}
              </div>
            </>
          )}

          {isShield && <ShieldModule />}

          {isApps && (
            <>
              <AppRouteSelect selectedRoute={selectedAppRoute} onSelect={onSelectAppRoute} />
              <div className="apps-grid" style={{
                marginTop: 24,
                display: 'grid',
                gridTemplateColumns: 'repeat(3, 1fr)',
                gap: 14,
              }}>
                {visibleApps.map((a) => (
                  <AppCard key={a.id} app={a} onPreview={onPreviewApp} />
                ))}
                {/* "Coming soon" placeholders only on the unfiltered
                    view — once the user picks an intent they're asking
                    to see specific apps, not aspirational ones. */}
                {!selectedAppRoute && (
                  <>
                    <ComingSoonAppCard key="coming-soon-1" />
                    <ComingSoonAppCard key="coming-soon-2" />
                  </>
                )}
              </div>
            </>
          )}

          {isShieldFunds && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
              <h4 style={{
                margin: 0,
                fontFamily: 'var(--mono)', fontWeight: 400,
                fontSize: 11.5, lineHeight: 1.4,
                letterSpacing: '0.14em', textTransform: 'uppercase',
                color: 'var(--text)',
              }}>
                How to shield and unshield assets
              </h4>
              {/* Video placeholder — swap the inner content for an
                  iframe / video element once the walkthrough is recorded. */}
              <div style={{
                position: 'relative',
                width: '100%',
                maxWidth: 920,
                aspectRatio: '16 / 9',
                background:
                  'radial-gradient(ellipse 60% 60% at 50% 40%, rgba(197,52,0,0.10), transparent 65%), ' +
                  'linear-gradient(180deg, rgba(20,20,20,0.85) 0%, rgba(10,10,10,0.9) 100%)',
                border: '1px solid var(--line)',
                borderRadius: 6,
                display: 'flex', flexDirection: 'column',
                alignItems: 'center', justifyContent: 'center',
                gap: 14,
              }}>
                <div aria-hidden="true" style={{
                  width: 64, height: 64,
                  borderRadius: '50%',
                  border: '1.5px solid var(--orange)',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  background: 'rgba(197,52,0,0.08)',
                }}>
                  <div style={{
                    width: 0, height: 0,
                    borderLeft: '18px solid var(--orange)',
                    borderTop: '12px solid transparent',
                    borderBottom: '12px solid transparent',
                    marginLeft: 5,
                  }} />
                </div>
                <span style={{
                  fontFamily: 'var(--mono)', fontSize: 11,
                  letterSpacing: '0.2em', textTransform: 'uppercase',
                  color: 'var(--dim)',
                }}>
                  Walkthrough video coming soon
                </span>
              </div>
            </div>
          )}

          {isShieldedMode && <ShieldedModeModule />}

          {!isGetWallet && !isBridge && !isShield && !isApps && !isShieldFunds && !isShieldedMode && (
            <p style={{
              margin: 0, fontFamily: 'var(--mono)', fontSize: 13,
              letterSpacing: '0.04em', lineHeight: 1.7, color: 'var(--dim)',
            }}>
              XYZ
            </p>
          )}
          </div>
        </div>
      </div>
    </div>
  )
}

function LiveApps() {
  // Default to tab 01 (Get Wallet) open on mount. Clicking the active
  // cell still closes the panel; clicking a different cell swaps it.
  const [activeTab, setActiveTab] = useState(0)
  const [selectedWallet, setSelectedWallet] = useState(null)
  const [selectedBridge, setSelectedBridge] = useState(null)
  const [selectedApp, setSelectedApp] = useState(null)
  // Bridge route filter — single-select dropdown. null = show all.
  const [selectedRoute, setSelectedRoute] = useState(null)
  const [selectedAppRoute, setSelectedAppRoute] = useState(null)
  // Clicking the active tab is a no-op — at least one section must
  // stay open so the panel area is never empty. Clicking a different
  // tab swaps the open section.
  const handleClick = (i) => setActiveTab((cur) => (cur === i ? cur : i))

  // styles.css applies a body::before scanline texture (repeating
  // horizontal lines, mix-blend-mode: multiply) on top of every page
  // via the --scan-op CSS variable. On the live-apps surface those
  // lines fall directly on buttons and titles, and the previous
  // attempt to suppress them via --scan-op: 0 raced with the Tweaks
  // panel which sets it back to 0.35 when the user toggles scanlines
  // on. We now add a body.no-scanlines class whose !important rule in
  // styles.css wins over any --scan-op change for as long as this
  // route is mounted. Class is removed on unmount so other pages
  // keep the texture intact.
  useEffect(() => {
    document.body.classList.add('no-scanlines')
    return () => document.body.classList.remove('no-scanlines')
  }, [])

  return (
    <>
      <main className="la-main" style={{ minHeight: '100vh', padding: 'clamp(96px,12vh,160px) 0 80px' }}>
        <div className="wrap">
          {/* The whole grid is one panel: 2px orange top accent + line
              border on the rest + 5px radius. Cells handle their own
              internal dividers (right + bottom borders) so the panel
              reads as one continuous tile divided into segments.
              Sticky from the top of the viewport so it stays in view
              as the content below scrolls. `top: 72px` sits just
              under the fixed HeaderApp nav. The orange progress fill
              already had `position: absolute` inside, so the sticky
              context here doesn't disrupt it. */}
          <div className="live-apps-tabs" style={{
            position: 'sticky',
            top: 72,
            zIndex: 40,
            display: 'grid',
            gridTemplateColumns: `repeat(${TABS.length}, 1fr)`,
            background:
              'radial-gradient(ellipse 60% 50% at 50% 100%, rgba(197,52,0,0.10), transparent 60%), ' +
              'linear-gradient(180deg, rgba(18,18,18,0.85) 0%, rgba(10,10,10,0.92) 100%)',
            border: '1px solid var(--line)',
            borderTop: '2px solid var(--orange)',
            borderRadius: 5,
            overflow: 'hidden',
            // Subtle shadow when stuck so the tab row visually
            // separates from the scrolling content underneath.
            boxShadow: '0 12px 28px -16px rgba(0,0,0,0.65)',
          }}>
            {/* Progress fill — a single horizontal element that grows
                left-to-right as the user clicks tabs. Width animates
                from 0 to ((activeTab + 1) / TABS.length) * 100%, so the
                clicked tab becomes the right edge of the fill. Going
                from tab 0 to tab 5 sweeps through all intermediate
                tabs in one continuous motion. */}
            <div aria-hidden="true" style={{
              position: 'absolute', top: 0, left: 0, bottom: 0,
              width: activeTab !== null ? `${((activeTab + 1) / TABS.length) * 100}%` : '0%',
              background: ORANGE_FILL_GRADIENT,
              transition: 'width 320ms cubic-bezier(.2, .8, .2, 1)',
              pointerEvents: 'none',
              zIndex: 0,
            }} />

            {TABS.map((tab, i) => (
              <TabCell
                key={tab.id}
                tab={tab}
                isActive={activeTab === i}
                isFilled={activeTab !== null && i <= activeTab}
                onClick={() => handleClick(i)}
                isRightEdge={i === TABS.length - 1}
                isBottomRow={true}
              />
            ))}
          </div>

          <SlideDownPanel
            activeTab={activeTab}
            tab={activeTab !== null ? TABS[activeTab] : null}
            onPreviewWallet={setSelectedWallet}
            onPreviewBridge={setSelectedBridge}
            selectedRoute={selectedRoute}
            onSelectRoute={setSelectedRoute}
            onPreviewApp={setSelectedApp}
            selectedAppRoute={selectedAppRoute}
            onSelectAppRoute={setSelectedAppRoute}
          />
        </div>
      </main>

      {selectedWallet && (
        <WalletSlidePanel
          wallet={selectedWallet}
          onClose={() => setSelectedWallet(null)}
        />
      )}
      {selectedBridge && (
        <BridgeSlidePanel
          bridge={selectedBridge}
          onClose={() => setSelectedBridge(null)}
        />
      )}
      {selectedApp && (
        <AppSlidePanel
          app={selectedApp}
          onClose={() => setSelectedApp(null)}
        />
      )}

      <style>{`
        /* ── Shared slide-panel animations ─────────────────────────
           Live here (not inside any individual panel) so both the
           wallet and bridge slide-ins can reach them. Without this,
           a panel rendered alone never receives onAnimationEnd for
           the close animation and can't unmount on overlay click. */
        @keyframes la-fade-in  { from { opacity: 0 } to { opacity: 1 } }
        @keyframes la-fade-out { from { opacity: 1 } to { opacity: 0 } }
        @keyframes la-slide-in  { from { transform: translateX(100%) } to { transform: translateX(0) } }
        @keyframes la-slide-out { from { transform: translateX(0) } to { transform: translateX(100%) } }
        /* Mobile bottom-sheet equivalents — same durations and easings
           as the desktop slide-in/out so the close-button path feels
           consistent across breakpoints. Wired up via the @media block
           further down: at ≤640px we swap the keyframe name and the
           positioning on .la-side-panel. */
        @keyframes la-sheet-up   { from { transform: translateY(100%) } to { transform: translateY(0) } }
        @keyframes la-sheet-down { from { transform: translateY(0) } to { transform: translateY(100%) } }
        /* Drag handle bar — hidden on desktop, shown on mobile via the
           media query below. Sits inside the aside as the first child. */
        .la-sheet-handle { display: none; }
        /* Per-tab-switch fade + rise. Triggered by remount via
           key={tab?.id} on .la-tab-content. Slightly longer and softer
           than the open/close fade so it reads as a deliberate
           swap, not a re-open. */
        @keyframes la-tab-swap {
          from { opacity: 0; transform: translateY(10px); }
          to   { opacity: 1; transform: translateY(0);     }
        }
        .la-tab-content {
          animation: la-tab-swap .42s cubic-bezier(.2, .7, .2, 1) both;
          /* Helps the GPU prep transform+opacity for the animation
             without leaving a stale compositing layer afterwards. */
          will-change: opacity, transform;
        }
        @media (prefers-reduced-motion: reduce) {
          .la-tab-content { animation: none; }
        }

        /* ── Bridge route dropdown ─────────────────────────────────
           Single trigger button + a menu that drops below it. The
           menu is always in the DOM so the open/close transition
           runs both ways; pointer-events is gated on data-open.
           Trigger is sized so the longest option (Official Ethereum
           → Starknet Bridge, uppercase + 0.14em tracking) fits on a
           single line. The menu inherits that width via min-width:
           100% and grows further via width: max-content if a future
           longer option is added. Options carry the same mono /
           uppercase / letter-spacing as the trigger. */
        .bridge-route {
          position: relative;
          display: inline-block;
          margin-top: 22px;
          min-width: 600px;
          max-width: 100%;
        }

        /* ── Bridge feature chips ──────────────────────────────────
           Small uppercase tags rendered above the bridge name. The
           --lg variant is used in the slide-in panel where there is
           more room. Same orange brand tint for visual cohesion. */
        .bridge-chip-row {
          display: flex;
          flex-wrap: wrap;
          gap: 6px;
        }
        .bridge-chip {
          display: inline-flex;
          align-items: center;
          font-family: var(--mono);
          font-size: 9.5px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--orange);
          background: rgba(197,52,0,0.08);
          border: 1px solid rgba(197,52,0,0.32);
          border-radius: 2px;
          padding: 3px 7px;
          white-space: nowrap;
          line-height: 1.2;
        }
        .bridge-chip-row--lg { gap: 8px; }
        .bridge-chip--lg {
          font-size: 11px;
          padding: 5px 10px;
        }
        .bridge-route__trigger {
          width: 100%;
          display: inline-flex;
          align-items: center;
          justify-content: space-between;
          gap: 18px;
          font-family: var(--mono);
          font-size: 11.5px;
          letter-spacing: 0.14em;
          text-transform: uppercase;
          color: var(--text);
          background: rgba(0,0,0,0.30);
          border: 1px solid var(--line);
          border-radius: 4px;
          padding: 18px 22px;
          cursor: pointer;
          transition:
            border-color .22s cubic-bezier(.2,.85,.25,1),
            color .22s cubic-bezier(.2,.85,.25,1),
            background .22s cubic-bezier(.2,.85,.25,1),
            box-shadow .22s ease-out;
        }
        .bridge-route__trigger:hover {
          border-color: var(--orange);
          color: var(--orange);
        }
        .bridge-route[data-open="true"] .bridge-route__trigger {
          border-color: var(--orange);
          color: var(--orange);
          box-shadow: 0 0 0 1px var(--orange) inset;
        }
        .bridge-route__label {
          overflow: hidden;
          white-space: nowrap;
          text-overflow: ellipsis;
        }
        .bridge-route__caret {
          font-size: 16px;
          line-height: 1;
          transition: transform .24s cubic-bezier(.2,.85,.25,1);
        }
        .bridge-route[data-open="true"] .bridge-route__caret {
          transform: rotate(180deg);
        }
        .bridge-route__menu {
          position: absolute;
          top: calc(100% + 8px);
          left: 0;
          min-width: 100%;
          width: max-content;
          max-width: min(640px, 92vw);
          z-index: 5;
          display: flex;
          flex-direction: column;
          padding: 8px;
          background: var(--bg);
          border: 1px solid var(--line);
          border-top: 2px solid var(--orange);
          border-radius: 4px;
          box-shadow: 0 18px 40px -12px rgba(0,0,0,0.55);
          /* Motion-only reveal: a clip-path curtain from the top
             plus a 4px translateY for spring. No opacity fade.
             Duration kept short so the open feels near-instant. */
          pointer-events: none;
          clip-path: inset(0 0 100% 0);
          transform: translateY(-4px);
          transition:
            clip-path .14s cubic-bezier(.2,.85,.25,1),
            transform .14s cubic-bezier(.2,.85,.25,1);
        }
        .bridge-route[data-open="true"] .bridge-route__menu {
          pointer-events: auto;
          clip-path: inset(0 0 0 0);
          transform: translateY(0);
        }
        .bridge-route__option {
          text-align: left;
          white-space: nowrap;
          font-family: var(--mono);
          font-size: 14px;
          letter-spacing: 0.14em;
          text-transform: uppercase;
          color: var(--text);
          background: transparent;
          border: none;
          border-radius: 3px;
          padding: 14px 18px;
          cursor: pointer;
          transition:
            background .18s ease-out,
            color .18s ease-out;
        }
        .bridge-route__option:hover {
          background: rgba(197,52,0,0.10);
          color: var(--orange);
        }
        .bridge-route__option[data-on="true"] {
          color: var(--orange);
          background: rgba(197,52,0,0.08);
        }
        .bridge-route__option--reset {
          color: var(--dim);
          font-size: 12px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          border-bottom: 1px dashed var(--line);
          border-radius: 0;
          margin-bottom: 6px;
          padding: 12px 18px;
        }
        .bridge-route__option--reset:hover {
          color: var(--green);
          background: transparent;
        }

        /* Animated multicolor gradient — cool-side palette only, no
           orange/red stops, so it stays readable when the orange
           progress fill sweeps under the cell.
           Stops are palindromic — yellow → green → cyan → blue →
           violet, then mirrored back — so the gradient has no hard
           seam where it wraps. The animation sweeps the same colors
           in both directions and the loop is invisible. */
        .live-apps-tab-title-glow {
          background: linear-gradient(
            90deg,
            #ffe34d 0%,
            #80ff80 12.5%,
            #40e0d0 25%,
            #4d9fff 37.5%,
            #b86dff 50%,
            #4d9fff 62.5%,
            #40e0d0 75%,
            #80ff80 87.5%,
            #ffe34d 100%
          );
          background-size: 300% 100%;
          -webkit-background-clip: text;
          background-clip: text;
          color: transparent;
          -webkit-text-fill-color: transparent;
          animation: live-apps-multicolor 3.6s linear infinite;
          /* Two-state filter — vivid by default, pastel when the
             orange fill is behind the cell. Listing the same three
             functions in both states lets the transition interpolate
             each value smoothly. */
          filter:
            drop-shadow(0 0 6px rgba(64,224,208,0.5))
            brightness(1)
            saturate(1);
          transition: filter .4s ease;
        }
        .live-apps-tab-title-glow--filled {
          filter:
            drop-shadow(0 0 5px rgba(255,255,255,0.55))
            brightness(1.5)
            saturate(0.55);
        }
        @keyframes live-apps-multicolor {
          0%   { background-position: 0% 50%; }
          100% { background-position: 100% 50%; }
        }

        /* Tab strip stays a single row of 5 at every width so all
           sections are reachable without horizontal scrolling. Below
           700px we swap to icon-only mode: the number ("01") and the
           text title hide, and the SVG icon paints in their place. The
           strip's center-aligned cells make each icon read as a clean
           tap target. The .la-tab-icon flex switch is what reveals the
           svg; the inline style sets display:none by default so we
           override it here with !important. */
        @media (max-width: 700px) {
          .live-apps-tabs > button {
            padding: 12px 4px !important;
            min-height: 60px !important;
            justify-content: center !important;
            align-items: center !important;
            gap: 0 !important;
          }
          .live-apps-tabs > button > .la-tab-number,
          .live-apps-tabs > button > .la-tab-title {
            display: none !important;
          }
          .live-apps-tabs > button > .la-tab-icon {
            display: flex !important;
          }
        }

        /* ── Mobile breathing room ─────────────────────────────────
           Three nested containers stack horizontal padding on desktop:
           .la-main (--gut), .wrap (--gut), .la-section-panel (28px).
           That's ~96px of total gutter on each side at mobile widths —
           too much. Tighten .la-main + .wrap to a single shared gutter
           and zero out the section panel's side padding + border on
           mobile so the cards can reach a sensible width. The orange
           top accent on the panel stays — it carries the "section" cue
           without boxing the cards in. */
        @media (max-width: 640px) {
          /* la-main no longer adds a horizontal gutter — .wrap (var(--gut)) is the
             single alignment source, so content lines up with the nav at every width. */
          .la-section-panel {
            padding-inline: 0 !important;
            border: none !important;
            border-radius: 0 !important;
            background: none !important;
            min-height: 0 !important;
          }
        }

        /* ── Wallet card layout on mobile ──────────────────────────
           Match the reference layout:
             ┌────────────────────────────────────┐
             │ [logo]  NAME            [X][globe] │
             │         [ category ]               │
             │                                    │
             │ Description text on multiple lines │
             │                                    │
             │ [Tutorial] [Preview] [Download]    │
             └────────────────────────────────────┘
           Implemented with CSS grid on the card root. The header/footer
           wrappers become display:contents so their children participate
           in the root's grid directly, no JSX restructuring needed.
           The desktop banner image is dropped — too tall for the
           compact layout and the logo+name carries enough identity. */
        @media (max-width: 640px) {
          .la-product-card {
            display: grid !important;
            grid-template-columns: auto 1fr auto;
            grid-template-areas:
              "logo  info     socials"
              "desc  desc     desc"
              "acts  acts     acts";
            column-gap: 14px;
            row-gap: 14px;
            padding: 18px 18px 16px;
            min-height: 0 !important;
            align-items: start;
          }
          /* Header + its inner flex row dissolve so logo & name-block
             land directly in the root grid. Background image is killed
             and the bottom border is removed. */
          .la-product-card__header {
            display: contents !important;
          }
          .la-product-card__header > div { display: contents !important; }
          .la-product-card__header > div > div:first-child {
            grid-area: logo !important;
            align-self: start;
          }
          .la-product-card__name-block {
            grid-area: info !important;
            align-self: center;
            min-width: 0;
          }
          .la-product-card__category { display: block !important; }
          /* Desktop spacer (Bridge / App cards use a flex:1 div to
             push the footer to the bottom) is irrelevant in grid mode
             and would auto-place into a new row. Hide it. */
          .la-product-card__spacer { display: none !important; }

          /* Description: re-show, drop its desktop padding, sit in
             "desc" row. */
          .la-product-card__body {
            display: block !important;
            grid-area: desc !important;
            padding: 0 !important;
            flex: none !important;
          }
          /* Footer is reduced to a transparent grid pass-through. Its
             socials head up to the top-right; its actions fill the
             bottom row, left-aligned. */
          .la-product-card .la-card-footer {
            display: contents !important;
          }
          .la-product-card .la-card-footer__socials {
            grid-area: socials !important;
            align-self: start;
            justify-self: end;
            background: transparent !important;
            border: none !important;
            padding: 0 !important;
          }
          .la-product-card .la-card-footer__actions {
            grid-area: acts !important;
            justify-content: flex-start !important;
            flex-wrap: wrap;
            width: auto !important;
          }
          .la-product-card .la-card-footer__actions > a,
          .la-product-card .la-card-footer__actions > button {
            flex: 0 0 auto !important;
            min-width: 0 !important;
          }
        }
        /* Wallet cards collapse to one column on narrower viewports
           so they don't get squashed. Mobile polish lands later. */
        @media (max-width: 720px) {
          .wallets-grid { grid-template-columns: 1fr !important; }
        }
        /* Bridge grid: 3 cols on wide → 2 cols on medium → 1 col on
           narrow. Apps grid follows the exact same breakpoints so
           both panels feel like the same surface. */
        @media (max-width: 1100px) {
          .bridges-grid { grid-template-columns: repeat(2, 1fr) !important; }
          .apps-grid    { grid-template-columns: repeat(2, 1fr) !important; }
        }
        @media (max-width: 720px) {
          .bridges-grid { grid-template-columns: 1fr !important; }
          .apps-grid    { grid-template-columns: 1fr !important; }
        }
        /* Shield tab 3-col layout (form / supported tokens / private
           DeFi) collapses to 1 col on medium screens so the side
           panels stay readable. */
        @media (max-width: 1100px) {
          .shield-tab-grid { grid-template-columns: 1fr !important; max-width: 520px; margin: 0 auto; }
        }

        /* ── Card footer: stack + wrap actions on mobile ───────────
           At ≤640px the card collapses to 1-col so the footer's three
           buttons (Tutorial / Preview / Download on WalletCard, or
           Preview / Launch app on Bridge/App) need to lay out without
           horizontal overflow. We stack socials on top, then let the
           action buttons fill the row and wrap if there are 3 of them.
           Buttons get flex: 1 with a sensible min-width so two fit on
           one row and a third drops to a new line. */
        @media (max-width: 640px) {
          .la-card-footer {
            flex-direction: column !important;
            align-items: stretch !important;
            gap: 10px !important;
          }
          .la-card-footer__socials {
            justify-content: flex-start;
          }
          .la-card-footer__actions {
            justify-content: stretch !important;
            flex-wrap: wrap;
            width: 100%;
          }
          .la-card-footer__actions > a,
          .la-card-footer__actions > button {
            flex: 1 1 auto;
            min-width: 96px;
            justify-content: center;
          }
        }

        /* ── Slide panel → bottom sheet on mobile ──────────────────
           At ≤640px the wallet / bridge / app detail panels stop
           sliding in from the right and become a bottom sheet:
           full-width, anchored to the bottom, max 90vh tall, with a
           drag handle on top. The inline styles in each panel describe
           the desktop side-aside layout — these overrides win via
           !important and the keyframe swap. data-closing="true" on the
           aside (set when the parent flips closing state) makes the
           dismiss keyframe play instead of the open one. */
        @media (max-width: 640px) {
          .la-side-panel {
            top: auto !important;
            left: 0 !important;
            right: 0 !important;
            bottom: 0 !important;
            width: 100% !important;
            max-height: 90vh;
            border-right: 1px solid var(--line) !important;
            border-bottom: none !important;
            border-top-left-radius: 14px !important;
            border-top-right-radius: 14px !important;
            box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.55) !important;
            animation-name: la-sheet-up !important;
            overscroll-behavior: contain;
          }
          .la-side-panel[data-closing="true"] {
            animation-name: la-sheet-down !important;
          }
          .la-sheet-handle {
            display: block;
            position: sticky;
            top: 0;
            width: 44px;
            height: 5px;
            margin: 10px auto 0;
            border-radius: 999px;
            background: rgba(255, 255, 255, 0.32);
            z-index: 3;
            pointer-events: none;
          }
        }
      `}</style>
    </>
  )
}

// No export — loaded as a script tag, LiveApps becomes a window
// global referenced by app.jsx's pathname switch.

/* ─────────────────────────────────────────────────────────────────
 * LEGACY APPS GRID — the previous AVNU / VESU / ENDUR / TROVES grid
 * + right-slide detail panel (banner header + circular profile-pic
 * logo + Publicly/Privately sections). Recoverable from git history:
 *
 *   git show efeae7d:live-apps.jsx
 *
 * Banner images are still on disk at public/live-apps-headers/ so a
 * future restore won't need to re-fetch the partner art.
 * ─────────────────────────────────────────────────────────────── */
