{"id":2,"date":"2026-07-02T07:37:58","date_gmt":"2026-07-02T07:37:58","guid":{"rendered":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/?page_id=2"},"modified":"2026-10-16T13:53:29","modified_gmt":"2026-10-16T13:53:29","slug":"sample-page","status":"publish","type":"page","link":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/?page_id=2","title":{"rendered":"[POISTA \u2013 vanha] Sample Page"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2\" class=\"elementor elementor-2\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1b7df5d e-flex e-con-boxed e-con e-parent\" data-id=\"1b7df5d\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-56cc60d elementor-widget elementor-widget-html\" data-id=\"56cc60d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!-- 1. Container for the widget -->\n<div id=\"aspa-widget-root\"><\/div>\n\n<!-- 2. Load React, ReactDOM, and Babel from CDN -->\n<script src=\"https:\/\/unpkg.com\/react@18\/umd\/react.production.min.js\" crossorigin><\/script>\n<script src=\"https:\/\/unpkg.com\/react-dom@18\/umd\/react-dom.production.min.js\" crossorigin><\/script>\n<script src=\"https:\/\/unpkg.com\/@babel\/standalone\/babel.min.js\"><\/script>\n\n<!-- 3. The Application Code -->\n<script type=\"text\/babel\">\nconst { useState, useRef, useEffect } = React;\n\n\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n   ASPA \u00b7 Asuntopalvelun avustaja \u2014 hi-fi chat widget\n   \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n\/\/ \u26a0\ufe0f DANGER: Exposing API keys in the browser is unsafe for production.\nconst ANTHROPIC_API_KEY = \"LAITA_OMA_AVAIN_TAHAN\"; \n\nconst C = {\n  blue: \"#0A84FF\",\n  blueDark: \"#0060DF\",\n  blueDeep: \"#0A2540\",\n  ink: \"#0B1524\",\n  sub: \"#68707e\",\n  line: \"#e8eaf0\",\n  surface: \"#ffffff\",\n  bg: \"#eef1f6\",\n  botBubble: \"#f1f3f8\",\n  chip: \"#eef4ff\",\n};\n\nconst FONT = '-apple-system, BlinkMacSystemFont, \"SF Pro Text\", \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\nconst GRADS = [\n  [\"#0A84FF\", \"#0060DF\"],\n  [\"#2B9BF4\", \"#0A6CD6\"],\n  [\"#12A3C7\", \"#0A6CD6\"],\n  [\"#4C6FFF\", \"#0A3FD6\"],\n  [\"#0AB2C7\", \"#0A84FF\"],\n  [\"#5B8DEF\", \"#2C4FE0\"],\n  [\"#12B3A6\", \"#0A84FF\"],\n  [\"#3A7BD5\", \"#12315e\"],\n];\n\nconst CATALOG = [\n  { id: \"liesharjunkatu-11\", address: \"Liesharjunkatu 11\", area: \"Sammonlahti\", rooms: [1, 3, 4], min: 260, max: 1116, free: 5, note: \"Vapautuu 15.08.\" },\n  { id: \"korpikunnaankatu-5\", address: \"Korpikunnaankatu 5\", area: \"Sammonlahti\", rooms: [2, 3], min: 533, max: 908, free: 5 },\n  { id: \"harjannekatu-3\", address: \"Harjannekatu 3\", area: \"Kivisalmi\", rooms: [1, 2, 3], min: 404, max: 830, free: 5 },\n  { id: \"joutsenkuja-4\", address: \"Joutsenkuja 4\", area: \"Joutseno\", rooms: [1, 2, 3], min: 407, max: 839, free: 2 },\n  { id: \"kurjentie-75\", address: \"Kurjentie 75\", area: \"M\u00e4ntyl\u00e4\", rooms: [1, 2, 3], min: 398, max: 842, free: 3 },\n  { id: \"maininkikatu-8\", address: \"Maininkikatu 8\", area: \"Kariniemi\", rooms: [2, 3], min: 488, max: 770, free: 2 },\n  { id: \"solkikuja-6-8\", address: \"Solkikuja 6-8\", area: \"Lampikangas\", rooms: [1, 2, 3, 4], min: 316, max: 823, free: 10 },\n  { id: \"voisalmentie-18\", address: \"Voisalmentie 18\", area: \"Voisalmi\", rooms: [1, 2, 3], min: 376, max: 751, free: 3 },\n];\n\nconst AREAS = [...new Set(CATALOG.map((p) => p.area))];\nconst byId = (id) => CATALOG.find((p) => p.id === id);\nconst fmt = (n) => Math.round(n).toLocaleString(\"fi-FI\");\nconst gradFor = (id) => {\n  const i = CATALOG.findIndex((p) => p.id === id);\n  return GRADS[(i < 0 ? 0 : i) % GRADS.length];\n};\n\nconst CONTACT = {\n  phone: \"0800 143 100\",\n  tel: \"tel:0800143100\",\n  email: \"aspainfo@aspakoti.fi\",\n  address: \"Valtakatu 44, 53100 Lappeenranta\",\n};\n\nconst buildSystem = (timeStr) => {\n  return `Olet \"ASPA\", Lappeenrannan Asuntopalvelun (Aspakoti) yst\u00e4v\u00e4llinen ja ammattimainen asumisavustaja verkkosivuilla.\n\nT\u00c4M\u00c4NHETKINEN AIKA (Europe\/Helsinki): ${timeStr}\n\nYRITYS: Lappeenrannan Asuntopalvelu Oy tarjoaa vuokra-asuntoja Lappeenrannan seudulla (ARA-tuettuja ja vapaarahoitteisia). Toimisto: ${CONTACT.address}. Puhelin: ${CONTACT.phone}. S\u00e4hk\u00f6posti: ${CONTACT.email}.\nASUNNON HAKU: t\u00e4ytt\u00e4m\u00e4ll\u00e4 asuntohakemuksen verkkosivuilla ja lis\u00e4\u00e4m\u00e4ll\u00e4 liitteet; kohteita lis\u00e4t\u00e4\u00e4n hakemukselle asuntohausta.\nVIKAILMOITUS: asukas tekee sen verkkosivujen \"Tee vikailmoitus\" -ohjeen mukaan.\n\nVAPAAT KOHTEET (ainoat joita saat ehdottaa; \u00e4l\u00e4 keksi muita):\n${CATALOG.map((p) => `- id:${p.id} | ${p.address}, ${p.area} | ${p.rooms.map((r) => r + \"h\").join(\", \")} | ${fmt(p.min)}\u2013${fmt(p.max)} \u20ac\/kk | ${p.free} vapaana${p.note ? \" | \" + p.note : \"\"}`).join(\"\\n\")}\n\nVASTAA AINA pelk\u00e4ll\u00e4 JSON-objektilla, ei mit\u00e4\u00e4n muuta teksti\u00e4, ei koodiaitoja. Muoto:\n{\"reply\": \"<lyhyt, kohtelias vastaus kysyj\u00e4n kielell\u00e4>\", \"propertyIds\": [\"<catalog id>\", ...], \"suggestContact\": <true|false>}\n\nS\u00c4\u00c4NN\u00d6T:\n- Tunnista kysyj\u00e4n kieli (suomi\/englanti\/ven\u00e4j\u00e4 tms.) ja vastaa samalla kielell\u00e4.\n- Kun kysyj\u00e4 etsii asuntoa, valitse propertyIds vain yll\u00e4 olevasta listasta osuvuuden mukaan (huoneluku, alue, hinta). Muuten propertyIds on tyhj\u00e4.\n- \u00c4l\u00e4 koskaan keksi kohteita, hintoja tai vapaustilanteita. Muistuta tarvittaessa ett\u00e4 tilanne voi muuttua ja ajantasaisen n\u00e4kee asuntohausta.\n- Pid\u00e4 reply lyhyen\u00e4 ja ammattimaisena (1\u20133 lausetta). Kellonajan ja p\u00e4iv\u00e4m\u00e4\u00e4r\u00e4n saat yll\u00e4 olevasta ajasta.\n- Jos kysyj\u00e4 haluaa puhua ihmiselle tai kysyy yhteystietoja, aseta suggestContact=true.\n- T\u00e4ysin asumiseen liittym\u00e4tt\u00f6miin kysymyksiin vastaa lyhyesti mutta ohjaa takaisin asumisaiheisiin.`;\n};\n\n\/* \u2500\u2500 main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst AspaWidget = () => {\n  const [open, setOpen] = useState(false);\n  const [view, setView] = useState(\"chat\"); \n  const [filterOpen, setFilterOpen] = useState(false);\n  const [messages, setMessages] = useState([\n    {\n      role: \"assistant\",\n      text: \"Hei! Olen ASPA, Asuntopalvelun avustaja. Autan sinua l\u00f6yt\u00e4m\u00e4\u00e4n kodin Lappeenrannan seudulta. Kerro toiveesi omin sanoin \u2014 tai tarkenna hakua alta.\",\n      quick: true,\n    },\n  ]);\n  const [input, setInput] = useState(\"\");\n  const [loading, setLoading] = useState(false);\n  const [emailPrefill, setEmailPrefill] = useState(\"\");\n  const scrollRef = useRef(null);\n  const inputRef = useRef(null);\n\n  useEffect(() => {\n    if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\n  }, [messages, loading, view, filterOpen]);\n\n  const pushUser = (text) => {\n    setMessages((m) => [...m, { role: \"user\", text }]);\n  };\n\n  const ask = async (text) => {\n    const content = (text ?? input).trim();\n    if (!content || loading) return;\n    setInput(\"\");\n    setFilterOpen(false);\n    const history = [...messages, { role: \"user\", text: content }];\n    setMessages(history);\n    setLoading(true);\n\n    const timeStr = new Date().toLocaleString(\"fi-FI\", {\n      timeZone: \"Europe\/Helsinki\",\n      weekday: \"long\", day: \"numeric\", month: \"long\", year: \"numeric\",\n      hour: \"2-digit\", minute: \"2-digit\",\n    });\n\n    try {\n      const res = await fetch(\"https:\/\/api.anthropic.com\/v1\/messages\", {\n        method: \"POST\",\n        headers: { \n          \"Content-Type\": \"application\/json\",\n          \"x-api-key\": ANTHROPIC_API_KEY,\n          \"anthropic-version\": \"2023-06-01\",\n          \"anthropic-dangerously-allow-browser\": \"true\" \n        },\n        body: JSON.stringify({\n          model: \"claude-3-5-sonnet-20240620\", \n          max_tokens: 1000,\n          system: buildSystem(timeStr),\n          messages: history\n            .filter((m) => m.text)\n            .map((m) => ({ role: m.role, content: m.text })),\n        }),\n      });\n      const data = await res.json();\n      \n      const raw = (data.content || [])\n        .map((b) => (b.type === \"text\" ? b.text : \"\"))\n        .join(\"\")\n        .trim();\n        \n      const parsed = parseModel(raw);\n      const cards = parsed.ids.map(byId).filter(Boolean);\n      \n      setMessages((m) => [\n        ...m,\n        {\n          role: \"assistant\",\n          text: parsed.reply || \"Voisitko tarkentaa hieman, niin autan mielell\u00e4ni.\",\n          cards: cards.length ? cards : undefined,\n          contact: parsed.contact,\n        },\n      ]);\n    } catch (e) {\n      console.error(e);\n      setMessages((m) => [\n        ...m,\n        {\n          role: \"assistant\",\n          text: \"Pahoittelut, yhteydess\u00e4 oli h\u00e4iri\u00f6. Voit yritt\u00e4\u00e4 uudelleen tai olla yhteydess\u00e4 asiakaspalveluun numerossa \" + CONTACT.phone + \".\",\n        },\n      ]);\n    } finally {\n      setLoading(false);\n      requestAnimationFrame(() => inputRef.current && inputRef.current.focus());\n    }\n  };\n\n  const runFilter = ({ rooms, price, areas }) => {\n    const label = \"Etsin: \" + [\n        rooms.length ? rooms.map((r) => r + \"h\").join(\"\/\") : \"kaikki koot\",\n        price >= 1200 ? \"ei hintarajaa\" : \"enint\u00e4\u00e4n \" + fmt(price) + \" \u20ac\/kk\",\n        areas.length ? areas.join(\", \") : \"kaikki alueet\",\n      ].join(\" \u00b7 \");\n    pushUser(label);\n    setFilterOpen(false);\n\n    const results = CATALOG.filter((p) => {\n      const roomOk = rooms.length === 0 || p.rooms.some((r) => rooms.includes(r) || (rooms.includes(4) && r >= 4));\n      const areaOk = areas.length === 0 || areas.includes(p.area);\n      const priceOk = price >= 1200 || p.min <= price;\n      return roomOk && areaOk && priceOk;\n    }).sort((a, b) => a.min - b.min);\n\n    setTimeout(() => {\n      setMessages((m) => [\n        ...m,\n        results.length\n          ? {\n              role: \"assistant\",\n              text: \"L\u00f6ysin \" + results.length + \" kohdetta toiveisiisi. Selaa kortteja pyyhk\u00e4isem\u00e4ll\u00e4.\",\n              cards: results,\n            }\n          : {\n              role: \"assistant\",\n              text: \"N\u00e4ill\u00e4 ehdoilla ei juuri nyt l\u00f6ytynyt vapaata kohdetta. Kokeile v\u00e4ljent\u00e4\u00e4 hakua \u2014 tai j\u00e4t\u00e4 yhteystietosi, niin ilmoitamme heti kun sopiva vapautuu.\",\n              contact: true,\n            },\n      ]);\n    }, 260);\n  };\n\n  const askAbout = (p) => { ask(\"Kerro lis\u00e4\u00e4 kohteesta \" + p.address + \" (\" + p.area + \").\"); };\n  \n  const applyFor = (p) => {\n    setEmailPrefill(\"Hei,\\n\\nOlen kiinnostunut kohteesta \" + p.address + \", \" + p.area + \". Voisitteko olla yhteydess\u00e4 hakemiseen liittyen?\\n\\nTerveisin,\");\n    setView(\"email\");\n  };\n\n  const handleQuick = (kind) => {\n    if (kind === \"search\") setFilterOpen(true);\n    else if (kind === \"human\") setView(\"contact\");\n    else if (kind === \"email\") { setEmailPrefill(\"\"); setView(\"email\"); }\n  };\n\n  return (\n    <div style={{ background: C.bg, fontFamily: FONT }} className=\"relative w-full min-h-screen overflow-hidden\">\n      <style dangerouslySetInnerHTML={{ __html: styles }}><\/style>\n\n      {\/* FAB *\/}\n      <button\n        onClick={() => setOpen(true)}\n        aria-label=\"Avaa ASPA-chat\"\n        className=\"aspa-fab\"\n        style={{\n          position: \"fixed\", right: 24, bottom: 24, width: 62, height: 62,\n          borderRadius: 20, border: \"none\", cursor: \"pointer\", zIndex: 40,\n          background: `linear-gradient(140deg, ${C.blue}, ${C.blueDark})`,\n          boxShadow: \"0 14px 34px -10px rgba(10,132,255,.65)\",\n          transform: open ? \"scale(.4)\" : \"scale(1)\",\n          opacity: open ? 0 : 1,\n          pointerEvents: open ? \"none\" : \"auto\",\n          transition: \"transform .32s cubic-bezier(.34,1.56,.64,1), opacity .22s ease\",\n        }}\n      >\n        <span style={{ position: \"absolute\", inset: 0, display: \"grid\", placeItems: \"center\" }}>\n          <ChatIcon \/>\n        <\/span>\n        <span className=\"aspa-pulse\" \/>\n      <\/button>\n\n      {\/* WIDGET *\/}\n      <div\n        className=\"aspa-widget\"\n        style={{\n          position: \"fixed\", right: 24, bottom: 24, zIndex: 45,\n          width: \"min(392px, calc(100vw - 32px))\",\n          height: \"min(660px, calc(100vh - 48px))\",\n          borderRadius: 26, overflow: \"hidden\", background: C.surface,\n          border: `1px solid ${C.line}`,\n          boxShadow: \"0 40px 90px -30px rgba(11,37,64,.55), 0 8px 24px -12px rgba(11,37,64,.35)\",\n          display: \"flex\", flexDirection: \"column\",\n          transformOrigin: \"bottom right\",\n          transform: open ? \"translateY(0) scale(1)\" : \"translateY(18px) scale(.92)\",\n          opacity: open ? 1 : 0,\n          pointerEvents: open ? \"auto\" : \"none\",\n          transition: \"transform .34s cubic-bezier(.34,1.3,.5,1), opacity .26s ease\",\n        }}\n      >\n        <Header\n          view={view}\n          onBack={() => setView(\"chat\")}\n          onContact={() => setView(\"contact\")}\n          onClose={() => setOpen(false)}\n        \/>\n\n        {view === \"contact\" && (\n          <ContactView onEmail={() => { setEmailPrefill(\"\"); setView(\"email\"); }} \/>\n        )}\n        {view === \"email\" && (\n          <EmailView prefill={emailPrefill} onDone={() => setView(\"chat\")} \/>\n        )}\n\n        {view === \"chat\" && (\n          <React.Fragment>\n            <div ref={scrollRef} className=\"aspa-scroll\" style={{ flex: 1, overflowY: \"auto\", background: C.bg, padding: \"16px 14px\" }}>\n              {messages.map((m, i) => (\n                <Message key={i} m={m} onQuick={handleQuick} onAsk={askAbout} onApply={applyFor} \/>\n              ))}\n              {loading && <Typing \/>}\n            <\/div>\n            <Composer\n              inputRef={inputRef}\n              input={input}\n              setInput={setInput}\n              onSend={() => ask()}\n              filterOpen={filterOpen}\n              toggleFilter={() => setFilterOpen((v) => !v)}\n            \/>\n            {filterOpen && <FilterPanel onSearch={runFilter} onClose={() => setFilterOpen(false)} \/>}\n          <\/React.Fragment>\n        )}\n      <\/div>\n    <\/div>\n  );\n};\n\n\/* \u2500\u2500 model parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst parseModel = (text) => {\n  let t = (text || \"\").trim().replace(\/^```(json)?\/i, \"\").replace(\/```$\/, \"\").trim();\n  const attempt = (s) => {\n    try {\n      const o = JSON.parse(s);\n      return {\n        reply: typeof o.reply === \"string\" ? o.reply : \"\",\n        ids: Array.isArray(o.propertyIds) ? o.propertyIds : [],\n        contact: !!o.suggestContact,\n      };\n    } catch (e) { return null; }\n  };\n  let r = attempt(t);\n  if (!r) {\n    const m = t.match(\/\\{[\\s\\S]*\\}\/);\n    if (m) r = attempt(m[0]);\n  }\n  return r || { reply: text, ids: [], contact: false };\n};\n\n\/* \u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst Header = ({ view, onBack, onContact, onClose }) => {\n  const sub = view === \"contact\" ? \"Yhteystiedot\" : view === \"email\" ? \"L\u00e4het\u00e4 viesti\" : \"Paikalla \u00b7 vastaa heti\";\n  return (\n    <div style={{\n      display: \"flex\", alignItems: \"center\", gap: 11, padding: \"12px 14px\",\n      background: \"rgba(255,255,255,.82)\", backdropFilter: \"blur(14px)\",\n      WebkitBackdropFilter: \"blur(14px)\", borderBottom: `1px solid ${C.line}`,\n    }}>\n      {view !== \"chat\" ? (\n        <button className=\"aspa-icbtn\" onClick={onBack} aria-label=\"Takaisin\">\n          <BackIcon \/>\n        <\/button>\n      ) : (\n        <div style={{\n          width: 38, height: 38, borderRadius: 12, flexShrink: 0,\n          background: `linear-gradient(140deg, ${C.blue}, ${C.blueDark})`,\n          display: \"grid\", placeItems: \"center\",\n          boxShadow: \"0 6px 14px -6px rgba(10,132,255,.7)\",\n        }}>\n          <KeyMark \/>\n        <\/div>\n      )}\n      <div style={{ flex: 1, minWidth: 0 }}>\n        <div style={{ fontWeight: 700, color: C.ink, fontSize: 16, letterSpacing: \"-.01em\" }}>ASPA<\/div>\n        <div style={{ fontSize: 12, color: C.sub, display: \"flex\", alignItems: \"center\", gap: 6 }}>\n          {view === \"chat\" && <span style={{ width: 7, height: 7, borderRadius: 9, background: \"#2fcb7a\", display: \"inline-block\" }} \/>}\n          {sub}\n        <\/div>\n      <\/div>\n      {view === \"chat\" && (\n        <button className=\"aspa-icbtn\" onClick={onContact} aria-label=\"Puhu ihmiselle\">\n          <PersonIcon \/>\n        <\/button>\n      )}\n      <button className=\"aspa-icbtn\" onClick={onClose} aria-label=\"Sulje\">\n        <CloseIcon \/>\n      <\/button>\n    <\/div>\n  );\n};\n\n\/* \u2500\u2500 messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst Message = ({ m, onQuick, onAsk, onApply }) => {\n  const isUser = m.role === \"user\";\n  return (\n    <div className=\"aspa-in\" style={{ marginBottom: 12, display: \"flex\", flexDirection: \"column\", alignItems: isUser ? \"flex-end\" : \"flex-start\" }}>\n      <div style={{\n        maxWidth: \"86%\", padding: \"10px 13px\", fontSize: 14.5, lineHeight: 1.45,\n        whiteSpace: \"pre-wrap\",\n        background: isUser ? `linear-gradient(135deg, ${C.blue}, ${C.blueDark})` : C.surface,\n        color: isUser ? \"#fff\" : C.ink,\n        border: isUser ? \"none\" : `1px solid ${C.line}`,\n        borderRadius: 20,\n        borderBottomRightRadius: isUser ? 7 : 20,\n        borderBottomLeftRadius: isUser ? 20 : 7,\n        boxShadow: isUser ? \"0 8px 18px -10px rgba(10,132,255,.6)\" : \"0 4px 12px -8px rgba(11,37,64,.25)\",\n      }}>\n        {m.text}\n      <\/div>\n\n      {m.cards && <PropertyStack items={m.cards} onAsk={onAsk} onApply={onApply} \/>}\n\n      {m.contact && (\n        <button className=\"aspa-softbtn\" onClick={() => onQuick(\"human\")} style={{ marginTop: 8 }}>\n          <PersonIcon small \/> Puhu ihmiselle\n        <\/button>\n      )}\n\n      {m.quick && (\n        <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8, marginTop: 10 }}>\n          <QuickChip label=\"Etsi asuntoa\" onClick={() => onQuick(\"search\")} primary \/>\n          <QuickChip label=\"Puhu ihmiselle\" onClick={() => onQuick(\"human\")} \/>\n          <QuickChip label=\"J\u00e4t\u00e4 viesti\" onClick={() => onQuick(\"email\")} \/>\n        <\/div>\n      )}\n    <\/div>\n  );\n};\n\nconst QuickChip = ({ label, onClick, primary }) => {\n  return (\n    <button\n      onClick={onClick}\n      className=\"aspa-chip\"\n      style={{\n        fontSize: 13.5, fontWeight: 600, padding: \"8px 14px\", borderRadius: 999, cursor: \"pointer\",\n        background: primary ? `linear-gradient(135deg, ${C.blue}, ${C.blueDark})` : C.surface,\n        color: primary ? \"#fff\" : C.blue,\n        border: primary ? \"none\" : `1px solid ${C.line}`,\n        boxShadow: primary ? \"0 8px 18px -10px rgba(10,132,255,.6)\" : \"none\",\n      }}\n    >\n      {label}\n    <\/button>\n  );\n};\n\nconst Typing = () => {\n  return (\n    <div className=\"aspa-in\" style={{ marginBottom: 12 }}>\n      <div style={{ display: \"inline-flex\", gap: 5, alignItems: \"center\", padding: \"12px 15px\", background: C.surface, border: `1px solid ${C.line}`, borderRadius: 20, borderBottomLeftRadius: 7 }}>\n        {[0, 1, 2].map((d) => (\n          <span key={d} className=\"aspa-dot\" style={{ width: 7, height: 7, borderRadius: 9, background: C.blue, animationDelay: d * 0.15 + \"s\" }} \/>\n        ))}\n      <\/div>\n    <\/div>\n  );\n};\n\n\/* \u2500\u2500 property stack (swipeable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst PropertyStack = ({ items, onAsk, onApply }) => {\n  const [idx, setIdx] = useState(0);\n  const [drag, setDrag] = useState(0);\n  const [cw, setCw] = useState(0);\n  const start = useRef(null);\n  const wrap = useRef(null);\n  const many = items.length > 1;\n\n  useEffect(() => {\n    const el = wrap.current;\n    if (!el) return;\n    const set = () => setCw(el.offsetWidth);\n    set();\n    const ro = new ResizeObserver(set);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  const down = (e) => { start.current = e.clientX; try { e.currentTarget.setPointerCapture(e.pointerId); } catch (x) {} };\n  const move = (e) => { if (start.current == null) return; setDrag(e.clientX - start.current); };\n  const up = () => {\n    if (start.current == null) return;\n    const th = (cw || 300) * 0.16;\n    if (drag < -th && idx < items.length - 1) setIdx(idx + 1);\n    else if (drag > th && idx > 0) setIdx(idx - 1);\n    setDrag(0);\n    start.current = null;\n  };\n\n  return (\n    <div style={{ width: \"86%\", maxWidth: 320, marginTop: 10 }}>\n      <div ref={wrap} style={{ position: \"relative\", overflow: \"hidden\", borderRadius: 20, touchAction: \"pan-y\" }}>\n        {many && <div style={{ position: \"absolute\", inset: 0, transform: \"translateX(7px) scale(.965)\", background: \"#dfe4ee\", borderRadius: 20, zIndex: 0 }} \/>}\n        <div\n          onPointerDown={down} onPointerMove={move} onPointerUp={up} onPointerCancel={up} onPointerLeave={up}\n          style={{\n            display: \"flex\", position: \"relative\", zIndex: 1,\n            transform: `translateX(${-idx * cw + drag}px)`,\n            transition: start.current == null ? \"transform .38s cubic-bezier(.3,1.1,.4,1)\" : \"none\",\n            cursor: many ? \"grab\" : \"default\",\n          }}\n        >\n          {items.map((p) => (\n            <div key={p.id} style={{ flex: \"0 0 \" + (cw || 300) + \"px\", width: cw || 300 }}>\n              <PropertyCard p={p} onAsk={onAsk} onApply={onApply} \/>\n            <\/div>\n          ))}\n        <\/div>\n      <\/div>\n\n      {many && (\n        <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 8, marginTop: 9 }}>\n          <button className=\"aspa-navdot\" onClick={() => setIdx(Math.max(0, idx - 1))} disabled={idx === 0} aria-label=\"Edellinen\"><ChevronIcon dir=\"l\" \/><\/button>\n          <div style={{ display: \"flex\", gap: 5 }}>\n            {items.map((_, i) => (\n              <span key={i} onClick={() => setIdx(i)} style={{\n                width: i === idx ? 18 : 7, height: 7, borderRadius: 9, cursor: \"pointer\",\n                background: i === idx ? C.blue : \"#c7cedb\", transition: \"all .25s ease\",\n              }} \/>\n            ))}\n          <\/div>\n          <button className=\"aspa-navdot\" onClick={() => setIdx(Math.min(items.length - 1, idx + 1))} disabled={idx === items.length - 1} aria-label=\"Seuraava\"><ChevronIcon dir=\"r\" \/><\/button>\n        <\/div>\n      )}\n    <\/div>\n  );\n};\n\nconst PropertyCard = ({ p, onAsk, onApply }) => {\n  const [g1, g2] = gradFor(p.id);\n  return (\n    <div style={{ background: C.surface, border: `1px solid ${C.line}`, borderRadius: 20, overflow: \"hidden\", boxShadow: \"0 10px 26px -16px rgba(11,37,64,.4)\" }}>\n      <div style={{ position: \"relative\", height: 132, background: `linear-gradient(135deg, ${g1}, ${g2})` }}>\n        <div style={{ position: \"absolute\", inset: 0, opacity: .5, display: \"grid\", placeItems: \"center\" }}><BuildingMark \/><\/div>\n        <span style={{ position: \"absolute\", top: 10, left: 10, fontSize: 11.5, fontWeight: 600, color: \"#fff\", background: \"rgba(255,255,255,.22)\", padding: \"3px 9px\", borderRadius: 999, backdropFilter: \"blur(4px)\" }}>{p.area}<\/span>\n        <span style={{ position: \"absolute\", top: 10, right: 10, fontSize: 11.5, fontWeight: 700, color: g2, background: \"rgba(255,255,255,.94)\", padding: \"3px 9px\", borderRadius: 999 }}>{p.free} vapaana<\/span>\n        <div style={{ position: \"absolute\", left: 12, bottom: 10, right: 12, color: \"#fff\", fontWeight: 700, fontSize: 18, letterSpacing: \"-.01em\", textShadow: \"0 1px 8px rgba(0,0,0,.25)\" }}>{p.address}<\/div>\n      <\/div>\n      <div style={{ padding: 13 }}>\n        <div style={{ display: \"flex\", gap: 6, flexWrap: \"wrap\", marginBottom: 10 }}>\n          {p.rooms.map((r) => (\n            <span key={r} style={{ fontSize: 12, fontWeight: 600, color: C.blue, background: C.chip, padding: \"3px 9px\", borderRadius: 8 }}>{r}h<\/span>\n          ))}\n          {p.note && <span style={{ fontSize: 12, fontWeight: 600, color: \"#b26a00\", background: \"#fff3e0\", padding: \"3px 9px\", borderRadius: 8 }}>{p.note}<\/span>}\n        <\/div>\n        <div style={{ display: \"flex\", alignItems: \"baseline\", gap: 5, marginBottom: 12 }}>\n          <span style={{ fontSize: 20, fontWeight: 800, color: C.ink, letterSpacing: \"-.02em\" }}>{fmt(p.min)}\u2013{fmt(p.max)}<\/span>\n          <span style={{ fontSize: 13, color: C.sub, fontWeight: 600 }}>\u20ac\/kk<\/span>\n        <\/div>\n        <div style={{ display: \"flex\", gap: 8 }}>\n          <button className=\"aspa-primary\" style={{ flex: 1 }} onClick={() => onApply(p)}>J\u00e4t\u00e4 hakemus<\/button>\n          <button className=\"aspa-ghost\" onClick={() => onAsk(p)}>Kysy lis\u00e4\u00e4<\/button>\n        <\/div>\n      <\/div>\n    <\/div>\n  );\n};\n\n\/* \u2500\u2500 composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst Composer = ({ inputRef, input, setInput, onSend, filterOpen, toggleFilter }) => {\n  return (\n    <div style={{ padding: \"10px 12px\", background: C.surface, borderTop: `1px solid ${C.line}` }}>\n      <div style={{ display: \"flex\", alignItems: \"flex-end\", gap: 8 }}>\n        <button\n          onClick={toggleFilter}\n          aria-label=\"Tarkenna hakua\"\n          className=\"aspa-filterbtn\"\n          style={{ background: filterOpen ? C.blue : C.chip, color: filterOpen ? \"#fff\" : C.blue }}\n        >\n          <SlidersIcon \/>\n        <\/button>\n        <textarea\n          ref={inputRef}\n          rows={1}\n          value={input}\n          onChange={(e) => setInput(e.target.value)}\n          onKeyDown={(e) => { if (e.key === \"Enter\" && !e.shiftKey) { e.preventDefault(); onSend(); } }}\n          placeholder=\"Kirjoita viesti\u2026\"\n          className=\"aspa-textarea\"\n          style={{ flex: 1, resize: \"none\", maxHeight: 92, border: `1px solid ${C.line}`, background: C.bg, borderRadius: 18, padding: \"10px 13px\", fontSize: 14.5, color: C.ink, fontFamily: FONT }}\n        \/>\n        <button\n          onClick={onSend}\n          disabled={!input.trim()}\n          aria-label=\"L\u00e4het\u00e4\"\n          className=\"aspa-sendbtn\"\n          style={{ background: input.trim() ? `linear-gradient(135deg, ${C.blue}, ${C.blueDark})` : \"#d7dbe4\", cursor: input.trim() ? \"pointer\" : \"default\" }}\n        >\n          <SendIcon \/>\n        <\/button>\n      <\/div>\n    <\/div>\n  );\n};\n\n\/* \u2500\u2500 filter panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst FilterPanel = ({ onSearch, onClose }) => {\n  const [rooms, setRooms] = useState([]);\n  const [price, setPrice] = useState(1200);\n  const [areas, setAreas] = useState([]);\n  const toggle = (arr, set, v) => set(arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v]);\n\n  return (\n    <div className=\"aspa-sheet\" style={{ position: \"absolute\", left: 0, right: 0, bottom: 0, background: C.surface, borderTop: `1px solid ${C.line}`, borderTopLeftRadius: 22, borderTopRightRadius: 22, padding: \"16px 16px 18px\", boxShadow: \"0 -18px 40px -24px rgba(11,37,64,.4)\", zIndex: 5 }}>\n      <div style={{ width: 38, height: 5, borderRadius: 9, background: \"#dbe0ea\", margin: \"0 auto 14px\" }} \/>\n\n      <Label>Huoneita<\/Label>\n      <div style={{ display: \"flex\", gap: 8, marginBottom: 16 }}>\n        {[1, 2, 3, 4].map((r) => {\n          const on = rooms.includes(r);\n          return (\n            <button key={r} onClick={() => toggle(rooms, setRooms, r)} className=\"aspa-seg\" style={{ flex: 1, background: on ? C.blue : C.chip, color: on ? \"#fff\" : C.blue, borderColor: on ? C.blue : \"transparent\" }}>\n              {r}h{r === 4 ? \"+\" : \"\"}\n            <\/button>\n          );\n        })}\n      <\/div>\n\n      <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"baseline\", marginBottom: 4 }}>\n        <Label>Enimm\u00e4isvuokra<\/Label>\n        <span style={{ fontSize: 13.5, fontWeight: 700, color: C.blue }}>{price >= 1200 ? \"Ei yl\u00e4rajaa\" : fmt(price) + \" \u20ac\/kk\"}<\/span>\n      <\/div>\n      <input type=\"range\" min={200} max={1200} step={20} value={price} onChange={(e) => setPrice(+e.target.value)} style={{ width: \"100%\", accentColor: C.blue, marginBottom: 16 }} \/>\n\n      <Label>Alue<\/Label>\n      <div style={{ display: \"flex\", gap: 8, overflowX: \"auto\", paddingBottom: 6, marginBottom: 16 }} className=\"aspa-scroll\">\n        {AREAS.map((a) => {\n          const on = areas.includes(a);\n          return (\n            <button key={a} onClick={() => toggle(areas, setAreas, a)} className=\"aspa-seg\" style={{ whiteSpace: \"nowrap\", background: on ? C.blue : C.chip, color: on ? \"#fff\" : C.blue, borderColor: on ? C.blue : \"transparent\" }}>\n              {a}\n            <\/button>\n          );\n        })}\n      <\/div>\n\n      <div style={{ display: \"flex\", gap: 10 }}>\n        <button className=\"aspa-ghost\" onClick={onClose} style={{ flex: \"0 0 auto\" }}>Peruuta<\/button>\n        <button className=\"aspa-primary\" style={{ flex: 1 }} onClick={() => onSearch({ rooms, price, areas })}>N\u00e4yt\u00e4 asunnot<\/button>\n      <\/div>\n    <\/div>\n  );\n};\n\n\/* \u2500\u2500 contact view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst ContactView = ({ onEmail }) => {\n  return (\n    <div className=\"aspa-fade\" style={{ flex: 1, overflowY: \"auto\", background: C.bg, padding: 18 }}>\n      <div style={{ background: C.surface, border: `1px solid ${C.line}`, borderRadius: 22, padding: 20, textAlign: \"center\", boxShadow: \"0 10px 26px -18px rgba(11,37,64,.35)\" }}>\n        <div style={{ width: 60, height: 60, borderRadius: 18, margin: \"0 auto 12px\", background: `linear-gradient(140deg, ${C.blue}, ${C.blueDark})`, display: \"grid\", placeItems: \"center\", boxShadow: \"0 10px 22px -10px rgba(10,132,255,.7)\" }}>\n          <PersonIcon big \/>\n        <\/div>\n        <div style={{ fontWeight: 700, fontSize: 17, color: C.ink }}>Asiakaspalvelu<\/div>\n        <div style={{ fontSize: 13.5, color: C.sub, marginTop: 3, marginBottom: 18 }}>Autamme mielell\u00e4mme henkil\u00f6kohtaisesti.<\/div>\n\n        <ContactRow icon={<PhoneIcon \/>} label=\"Soita\" value={CONTACT.phone} href={CONTACT.tel} \/>\n        <ContactRow icon={<MailIcon \/>} label=\"S\u00e4hk\u00f6posti\" value={CONTACT.email} href={\"mailto:\" + CONTACT.email} \/>\n        <ContactRow icon={<PinIcon \/>} label=\"Toimisto\" value={CONTACT.address} \/>\n\n        <button className=\"aspa-primary\" style={{ width: \"100%\", marginTop: 8 }} onClick={onEmail}>L\u00e4het\u00e4 viesti t\u00e4st\u00e4<\/button>\n      <\/div>\n    <\/div>\n  );\n};\n\nconst ContactRow = ({ icon, label, value, href }) => {\n  const inner = (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"11px 12px\", background: C.bg, borderRadius: 14, marginBottom: 10, textAlign: \"left\" }}>\n      <span style={{ width: 34, height: 34, borderRadius: 10, background: C.chip, display: \"grid\", placeItems: \"center\", flexShrink: 0 }}>{icon}<\/span>\n      <span style={{ minWidth: 0 }}>\n        <span style={{ display: \"block\", fontSize: 11.5, color: C.sub, fontWeight: 600 }}>{label}<\/span>\n        <span style={{ display: \"block\", fontSize: 14.5, color: C.ink, fontWeight: 600, overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{value}<\/span>\n      <\/span>\n    <\/div>\n  );\n  return href ? <a href={href} style={{ textDecoration: \"none\" }}>{inner}<\/a> : inner;\n};\n\n\/* \u2500\u2500 email view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst EmailView = ({ prefill, onDone }) => {\n  const [name, setName] = useState(\"\");\n  const [email, setEmail] = useState(\"\");\n  const [phone, setPhone] = useState(\"\");\n  const [msg, setMsg] = useState(prefill || \"\");\n  const [sent, setSent] = useState(false);\n  const valid = name.trim() && \/\\S+@\\S+\\.\\S+\/.test(email) && msg.trim();\n\n  if (sent) {\n    return (\n      <div className=\"aspa-fade\" style={{ flex: 1, background: C.bg, display: \"grid\", placeItems: \"center\", padding: 24 }}>\n        <div style={{ textAlign: \"center\" }}>\n          <div className=\"aspa-check\" style={{ width: 72, height: 72, borderRadius: 24, margin: \"0 auto 16px\", background: `linear-gradient(140deg, ${C.blue}, ${C.blueDark})`, display: \"grid\", placeItems: \"center\", boxShadow: \"0 14px 30px -12px rgba(10,132,255,.7)\" }}>\n            <CheckIcon \/>\n          <\/div>\n          <div style={{ fontWeight: 700, fontSize: 18, color: C.ink }}>Kiitos, viesti l\u00e4hetetty!<\/div>\n          <div style={{ fontSize: 14, color: C.sub, marginTop: 6, maxWidth: 260 }}>Otamme sinuun yhteytt\u00e4 pian. Palaamme mielell\u00e4mme asuntoasioissasi.<\/div>\n          <button className=\"aspa-primary\" style={{ marginTop: 20, padding: \"10px 22px\" }} onClick={onDone}>Takaisin<\/button>\n        <\/div>\n      <\/div>\n    );\n  }\n\n  return (\n    <div className=\"aspa-fade aspa-scroll\" style={{ flex: 1, overflowY: \"auto\", background: C.bg, padding: 18 }}>\n      <div style={{ fontSize: 13.5, color: C.sub, marginBottom: 14 }}>J\u00e4t\u00e4 yhteystietosi ja viestisi, niin olemme sinuun yhteydess\u00e4.<\/div>\n      <Field label=\"Nimi *\"><input className=\"aspa-inp\" value={name} onChange={(e) => setName(e.target.value)} placeholder=\"Etunimi Sukunimi\" \/><\/Field>\n      <Field label=\"S\u00e4hk\u00f6posti *\"><input className=\"aspa-inp\" value={email} onChange={(e) => setEmail(e.target.value)} placeholder=\"nimi@esimerkki.fi\" inputMode=\"email\" \/><\/Field>\n      <Field label=\"Puhelin\"><input className=\"aspa-inp\" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder=\"040 123 4567\" inputMode=\"tel\" \/><\/Field>\n      <Field label=\"Viesti *\"><textarea className=\"aspa-inp\" value={msg} onChange={(e) => setMsg(e.target.value)} rows={4} placeholder=\"Kerro toiveistasi\u2026\" style={{ resize: \"none\" }} \/><\/Field>\n      <button className=\"aspa-primary\" style={{ width: \"100%\", marginTop: 4, opacity: valid ? 1 : .5, pointerEvents: valid ? \"auto\" : \"none\" }} onClick={() => setSent(true)}>L\u00e4het\u00e4 viesti<\/button>\n      <div style={{ fontSize: 11.5, color: C.sub, textAlign: \"center\", marginTop: 10 }}>Tietojasi k\u00e4sitell\u00e4\u00e4n luottamuksellisesti.<\/div>\n    <\/div>\n  );\n};\n\nconst Field = ({ label, children }) => {\n  return (\n    <div style={{ marginBottom: 13 }}>\n      <div style={{ fontSize: 12.5, fontWeight: 600, color: C.sub, marginBottom: 5 }}>{label}<\/div>\n      {children}\n    <\/div>\n  );\n};\nconst Label = ({ children }) => {\n  return <div style={{ fontSize: 12.5, fontWeight: 700, color: C.ink, marginBottom: 8, letterSpacing: \".01em\" }}>{children}<\/div>;\n};\n\n\/* \u2500\u2500 icons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst ChatIcon = () => (<svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v7A2.5 2.5 0 0 1 17.5 16H9l-4 3.5V16H6.5\" stroke=\"#fff\" strokeWidth=\"2\" strokeLinejoin=\"round\" strokeLinecap=\"round\"\/><circle cx=\"9\" cy=\"10\" r=\"1.1\" fill=\"#fff\"\/><circle cx=\"12.5\" cy=\"10\" r=\"1.1\" fill=\"#fff\"\/><circle cx=\"16\" cy=\"10\" r=\"1.1\" fill=\"#fff\"\/><\/svg>);\nconst KeyMark = () => (<svg width=\"19\" height=\"19\" viewBox=\"0 0 24 24\" fill=\"none\"><circle cx=\"12\" cy=\"8.5\" r=\"3.6\" stroke=\"#fff\" strokeWidth=\"2\"\/><path d=\"M10.6 11.6 9.5 19h5l-1.1-7.4\" stroke=\"#fff\" strokeWidth=\"2\" strokeLinejoin=\"round\"\/><\/svg>);\nconst BuildingMark = () => (<svg width=\"80\" height=\"80\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M4 20V9l8-5 8 5v11\" stroke=\"#fff\" strokeWidth=\"1.4\" strokeLinejoin=\"round\"\/><rect x=\"8\" y=\"12\" width=\"3\" height=\"3\" stroke=\"#fff\" strokeWidth=\"1.2\"\/><rect x=\"13\" y=\"12\" width=\"3\" height=\"3\" stroke=\"#fff\" strokeWidth=\"1.2\"\/><path d=\"M10.5 20v-3h3v3\" stroke=\"#fff\" strokeWidth=\"1.2\"\/><\/svg>);\nconst CloseIcon = () => (<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6l12 12M18 6L6 18\" stroke={C.ink} strokeWidth=\"2\" strokeLinecap=\"round\"\/><\/svg>);\nconst BackIcon = () => (<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 5l-7 7 7 7\" stroke={C.ink} strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\/><\/svg>);\nconst PersonIcon = ({ big, small }) => { const s = big ? 28 : small ? 15 : 18; const col = big ? \"#fff\" : C.ink; return (<svg width={s} height={s} viewBox=\"0 0 24 24\" fill=\"none\"><circle cx=\"12\" cy=\"8\" r=\"3.4\" stroke={col} strokeWidth=\"2\"\/><path d=\"M5.5 19a6.5 6.5 0 0 1 13 0\" stroke={col} strokeWidth=\"2\" strokeLinecap=\"round\"\/><\/svg>); };\nconst SendIcon = () => (<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M4 12 20 4l-4 16-4-7-8-1Z\" stroke=\"#fff\" strokeWidth=\"2\" strokeLinejoin=\"round\" strokeLinecap=\"round\"\/><\/svg>);\nconst SlidersIcon = () => (<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M4 7h10M18 7h2M4 12h4M12 12h8M4 17h12M20 17h0\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\"\/><circle cx=\"15\" cy=\"7\" r=\"2\" fill=\"currentColor\"\/><circle cx=\"9\" cy=\"12\" r=\"2\" fill=\"currentColor\"\/><circle cx=\"17\" cy=\"17\" r=\"2\" fill=\"currentColor\"\/><\/svg>);\nconst ChevronIcon = ({ dir }) => (<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" style={{ transform: dir === \"l\" ? \"rotate(180deg)\" : \"none\" }}><path d=\"M9 5l7 7-7 7\" stroke={C.blue} strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\/><\/svg>);\nconst PhoneIcon = () => (<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 3h3l1.5 5-2 1.5a11 11 0 0 0 5 5l1.5-2 5 1.5v3a2 2 0 0 1-2 2A16 16 0 0 1 4 5a2 2 0 0 1 2-2Z\" stroke={C.blue} strokeWidth=\"1.8\" strokeLinejoin=\"round\"\/><\/svg>);\nconst MailIcon = () => (<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2.5\" stroke={C.blue} strokeWidth=\"1.8\"\/><path d=\"m4 7 8 5 8-5\" stroke={C.blue} strokeWidth=\"1.8\" strokeLinejoin=\"round\"\/><\/svg>);\nconst PinIcon = () => (<svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M12 21s7-5.5 7-11a7 7 0 1 0-14 0c0 5.5 7 11 7 11Z\" stroke={C.blue} strokeWidth=\"1.8\" strokeLinejoin=\"round\"\/><circle cx=\"12\" cy=\"10\" r=\"2.4\" stroke={C.blue} strokeWidth=\"1.8\"\/><\/svg>);\nconst CheckIcon = () => (<svg width=\"34\" height=\"34\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M5 13l4 4L19 7\" stroke=\"#fff\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\/><\/svg>);\n\n\/* \u2500\u2500 styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst styles = `\n@keyframes aspaIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }\n@keyframes aspaDot { 0%,60%,100%{transform:translateY(0);opacity:.4} 30%{transform:translateY(-5px);opacity:1} }\n@keyframes aspaPulse { 0%{transform:scale(1);opacity:.5} 70%,100%{transform:scale(1.9);opacity:0} }\n@keyframes aspaSheet { from{transform:translateY(100%)} to{transform:translateY(0)} }\n@keyframes aspaFade { from{opacity:0} to{opacity:1} }\n@keyframes aspaCheck { 0%{transform:scale(.4);opacity:0} 60%{transform:scale(1.12)} 100%{transform:scale(1);opacity:1} }\n.aspa-in{ animation:aspaIn .3s ease both; }\n.aspa-dot{ display:inline-block; animation:aspaDot 1.1s infinite ease-in-out; }\n.aspa-sheet{ animation:aspaSheet .3s cubic-bezier(.3,1.1,.4,1) both; }\n.aspa-fade{ animation:aspaFade .28s ease both; }\n.aspa-check{ animation:aspaCheck .5s cubic-bezier(.3,1.4,.5,1) both; }\n.aspa-pulse{ position:absolute; inset:0; border-radius:20px; background:${C.blue}; animation:aspaPulse 2.6s infinite ease-out; z-index:-1; }\n.aspa-fab:hover{ filter:brightness(1.05); }\n.aspa-fab:active{ transform:scale(.94)!important; }\n.aspa-scroll::-webkit-scrollbar{ width:7px; height:7px; }\n.aspa-scroll::-webkit-scrollbar-thumb{ background:#d3d9e4; border-radius:8px; }\n.aspa-icbtn{ width:34px; height:34px; border-radius:11px; border:none; background:${C.bg}; display:grid; place-items:center; cursor:pointer; flex-shrink:0; transition:background .15s ease, transform .1s ease; }\n.aspa-icbtn:hover{ background:#e3e7ef; }\n.aspa-icbtn:active{ transform:scale(.9); }\n.aspa-primary{ border:none; border-radius:13px; padding:11px 16px; font-size:14.5px; font-weight:700; color:#fff; cursor:pointer; background:linear-gradient(135deg, ${C.blue}, ${C.blueDark}); box-shadow:0 10px 22px -12px rgba(10,132,255,.75); transition:transform .12s ease, filter .15s ease; font-family:${FONT}; }\n.aspa-primary:hover{ filter:brightness(1.04); }\n.aspa-primary:active{ transform:scale(.97); }\n.aspa-ghost{ border:1px solid ${C.line}; border-radius:13px; padding:11px 14px; font-size:14px; font-weight:600; color:${C.blue}; background:${C.surface}; cursor:pointer; transition:background .15s ease, transform .12s ease; font-family:${FONT}; }\n.aspa-ghost:hover{ background:${C.chip}; }\n.aspa-ghost:active{ transform:scale(.97); }\n.aspa-softbtn{ display:inline-flex; align-items:center; gap:7px; border:1px solid ${C.line}; background:${C.surface}; color:${C.blue}; font-weight:600; font-size:13.5px; padding:8px 14px; border-radius:999px; cursor:pointer; font-family:${FONT}; transition:background .15s ease; }\n.aspa-softbtn:hover{ background:${C.chip}; }\n.aspa-chip{ transition:transform .12s ease, filter .15s ease; font-family:${FONT}; }\n.aspa-chip:hover{ filter:brightness(1.03); }\n.aspa-chip:active{ transform:scale(.96); }\n.aspa-seg{ border:1px solid transparent; border-radius:12px; padding:9px 12px; font-size:13.5px; font-weight:700; cursor:pointer; transition:transform .1s ease, filter .15s ease; font-family:${FONT}; }\n.aspa-seg:active{ transform:scale(.95); }\n.aspa-navdot{ width:28px; height:28px; border-radius:9px; border:1px solid ${C.line}; background:${C.surface}; display:grid; place-items:center; cursor:pointer; transition:background .15s ease; }\n.aspa-navdot:hover:not(:disabled){ background:${C.chip}; }\n.aspa-navdot:disabled{ opacity:.35; cursor:default; }\n.aspa-filterbtn{ width:40px; height:40px; border-radius:14px; border:none; display:grid; place-items:center; cursor:pointer; flex-shrink:0; transition:transform .12s ease, background .18s ease; }\n.aspa-filterbtn:active{ transform:scale(.92); }\n.aspa-sendbtn{ width:40px; height:40px; border-radius:14px; border:none; display:grid; place-items:center; flex-shrink:0; transition:transform .12s ease, filter .15s ease; }\n.aspa-sendbtn:active{ transform:scale(.92); }\n.aspa-textarea:focus{ outline:none; border-color:${C.blue}!important; }\n.aspa-inp{ width:100%; box-sizing:border-box; border:1px solid ${C.line}; background:${C.surface}; border-radius:13px; padding:11px 13px; font-size:14.5px; color:${C.ink}; font-family:${FONT}; transition:border-color .15s ease; }\n.aspa-inp:focus{ outline:none; border-color:${C.blue}; }\n@media (prefers-reduced-motion: reduce){\n  .aspa-in,.aspa-dot,.aspa-sheet,.aspa-fade,.aspa-check,.aspa-pulse{ animation:none!important; }\n}\n`;\n\n\/\/ 4. Mount the React App\nconst rootElement = document.getElementById(\"aspa-widget-root\");\nconst root = ReactDOM.createRoot(rootElement);\nroot.render(<AspaWidget \/>);\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"open","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-2","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=\/wp\/v2\/pages\/2","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2"}],"version-history":[{"count":4,"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=\/wp\/v2\/pages\/2\/revisions"}],"predecessor-version":[{"id":83,"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=\/wp\/v2\/pages\/2\/revisions\/83"}],"wp:attachment":[{"href":"https:\/\/funny-magenta-eagle.157-180-92-33.cpanel.site\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}