From 0a0b9d964ec5dd1d5f73fe6c8791a8d16d56369b Mon Sep 17 00:00:00 2001 From: Morrow Contributors Date: Thu, 6 Aug 2026 20:32:09 +0800 Subject: [PATCH 1/6] fix: fix mobile support intake flow Ported from AbleDesk PR #47 (583b0e9). Help always opens at its landing page: a browser refresh is a new visit, not a restored transcript, so the stored session key is gone. Contact-card turns are hidden from the visible transcript, and the mobile intake shell is corrected. Session/reset storage keys keep the Morrow prefix. public/voice-demo.js and public/voice-demo.css are build outputs and are rebuilt separately. --- apps/desk/public/voice-demo.css | 2 +- apps/desk/src/voice/contact.ts | 4 +- apps/desk/src/voice/conversation.ts | 10 ++++- apps/desk/src/voice/demo-client.ts | 29 +++++++------ apps/desk/src/voice/demo-page.ts | 6 +-- apps/desk/src/voice/demo.css | 30 ++++++++++++-- apps/desk/test/visual/portal.visual.spec.ts | 46 +++++++++++++++++++-- apps/desk/test/voice-conversation.test.ts | 12 ++++++ apps/desk/test/voice-demo.test.ts | 1 + 9 files changed, 111 insertions(+), 29 deletions(-) diff --git a/apps/desk/public/voice-demo.css b/apps/desk/public/voice-demo.css index 294c996..fda5334 100644 --- a/apps/desk/public/voice-demo.css +++ b/apps/desk/public/voice-demo.css @@ -1 +1 @@ -:root{color-scheme:light;font-family:var(--font, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif);color:var(--ink, #121212);background:var(--canvas, #ffffff);font-synthesis:none;--card: #ffffff;--soft: color-mix(in srgb, var(--ink, #121212) 4%, #ffffff);--line: color-mix(in srgb, var(--ink, #121212) 12%, transparent);--line-strong: color-mix(in srgb, var(--ink, #121212) 22%, transparent);--muted: color-mix(in srgb, var(--ink, #121212) 62%, var(--canvas, #ffffff));--accent-soft: color-mix(in srgb, var(--accent, #c87942) 9%, #ffffff);--accent-line: color-mix(in srgb, var(--accent, #c87942) 34%, transparent);--focus-ring: color-mix(in srgb, var(--accent, #c87942) 50%, transparent);--surface-width: 64rem;--answer-width: 47.5rem}*{box-sizing:border-box}html,body{min-height:100%}html.support-conversation,body.support-conversation{height:100%;overflow:hidden;overscroll-behavior:none}body{margin:0;min-width:320px;font-family:inherit;background:var(--canvas, #fff)}button,input{font:inherit}button{border:0;cursor:pointer;touch-action:manipulation}button:disabled{cursor:not-allowed;opacity:.45}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.skip-link{position:fixed;z-index:20;top:8px;left:8px;transform:translateY(-150%);border-radius:8px;padding:10px 14px;color:var(--ink, #121212);background:#fff;box-shadow:0 2px 12px #0000001f;font-weight:700}.skip-link:focus{transform:translateY(0)}.app{min-height:100dvh;display:flex;flex-direction:column}.app[data-view=conversation]{position:fixed;z-index:1;top:var(--support-viewport-top, 0px);right:0;left:0;width:100%;min-height:0;height:var(--support-viewport-height, 100dvh);max-height:var(--support-viewport-height, 100dvh);overflow:hidden}.chat-header{flex:none;padding-top:env(safe-area-inset-top);background:var(--card)}.chat-header-inner{width:min(var(--surface-width),100% - 40px);min-height:64px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.header-actions,.support-nav{display:flex;align-items:center;gap:8px}.support-nav a,.text-button{min-height:44px;display:inline-flex;align-items:center;border-radius:9px;padding:0 9px;color:var(--muted);background:transparent;font-size:13px;font-weight:650;text-decoration:none;white-space:nowrap}.support-nav a:hover,.text-button:not(:disabled):hover{color:var(--ink, #121212);background:var(--soft)}.brand{min-width:0;display:inline-flex;align-items:center;gap:10px;color:inherit;text-decoration:none}.brand-logo{display:block;height:28px;max-width:150px;object-fit:contain}.brand-mark,.avatar{flex:none;display:grid;place-items:center;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-size:11px;font-weight:800}.brand-mark{width:32px;height:32px;border-radius:9px}.avatar{width:28px;height:28px;border-radius:50%}.brand-text{min-width:0;display:grid;line-height:1.25}.brand-name{overflow:hidden;font-size:15px;font-weight:720;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.brand-sub{color:var(--muted);font-size:12px}.reconnect-banner{flex:none;padding:8px 16px;border-bottom:1px solid var(--accent-line);color:var(--ink, #121212);background:var(--accent-soft);text-align:center;font-size:13px;font-weight:650}.thread{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-padding-block:24px;scrollbar-color:var(--line-strong) transparent}.thread-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:36px 0 24px}.app[data-view=landing] .thread{flex:none;overflow:visible}.app[data-view=landing] .thread-inner{width:min(var(--surface-width),100% - 40px);padding:104px 0 80px}.landing-hero h1{max-width:18ch;margin:0;font-size:clamp(2.5rem,4vw,3rem);font-weight:650;letter-spacing:-.045em;line-height:1}.landing-lede{max-width:42rem;margin:20px 0 0;color:var(--muted);font-size:15px;line-height:1.55}.ask-composer{width:100%;min-height:52px;margin-top:30px;display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:13px;padding:4px 5px 4px 10px;background:#fff;box-shadow:0 1px #0000000a,0 6px 18px #0000000f}.ask-composer:focus-within{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--focus-ring),0 8px 24px #00000014}.ask-icon{flex:none;display:grid;place-items:center}.ask-icon .avatar{width:24px;height:24px;font-size:9px}.ask-composer input{flex:1 1 auto;min-width:0;min-height:44px;border:0;padding:0 3px;color:var(--ink, #121212);background:transparent;font-size:16px;outline:0}.ask-composer input::placeholder,.composer input::placeholder{color:var(--muted)}.ask-composer input:focus-visible{outline:0}.ask-composer .mic-button,.ask-send-button{width:44px;height:44px;min-height:44px;padding:0}.ask-composer .mic-button{border:0}.ask-send-button{border-radius:9px;display:grid;place-items:center}.landing-trust{margin:13px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.landing-status{min-height:18px;margin:6px 0 0;color:var(--ink, #121212);font-size:12px;font-weight:650;line-height:1.5}.topics-section{margin-top:72px}.topics-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.topics-heading h2{margin:0;font-size:21px;font-weight:670;letter-spacing:-.025em}.topics-heading>a{color:var(--muted);font-size:13px;font-weight:650}.topic-grid{margin-top:27px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));column-gap:56px;row-gap:42px}.topic-card h3{margin:0;font-size:14px;font-weight:720;line-height:1.35}.topic-card h3 a,.topic-card li a{color:var(--ink, #121212);text-decoration:none}.topic-card h3 a:hover,.topic-card li a:hover{text-decoration:underline;text-underline-offset:3px}.topic-card>p{margin:7px 0 0;color:var(--muted);font-size:12.5px;line-height:1.5}.topic-card ul{margin:12px 0 0;padding:0;list-style:none;display:grid;gap:8px}.topic-card li{font-size:13px;line-height:1.45}.topics-empty{grid-column:1 / -1;margin:0;color:var(--muted)}.human-help-button{position:fixed;z-index:5;right:18px;bottom:max(16px,env(safe-area-inset-bottom));min-height:48px;display:inline-flex;align-items:center;gap:9px;border:1px solid var(--line-strong);border-radius:12px;padding:0 15px 0 10px;color:var(--ink, #121212);background:#fff;box-shadow:0 4px 16px #00000017;font-size:13px;font-weight:700}.human-help-button:hover{transform:translateY(-1px);box-shadow:0 6px 20px #0000001f}.human-help-button .avatar{width:26px;height:26px;font-size:9px}.thread-list{margin:0;padding:0 0 36px;list-style:none;display:flex;flex-direction:column;gap:28px}.answer-turn{display:grid;gap:8px}.answer-turn--user{width:100%}.turn-label{margin:0;color:var(--muted);font-size:12px;font-weight:700}.turn-copy{color:var(--ink, #121212);font-size:15px;line-height:1.62}.turn-copy p{margin:0;overflow-wrap:anywhere}.answer-turn--user .turn-copy{font-size:clamp(1.35rem,4vw,1.5rem);font-weight:650;letter-spacing:-.025em;line-height:1.25}.answer-turn--user .turn-copy h2{margin:0;overflow-wrap:anywhere;font:inherit}.answer-turn--pending{opacity:.65}.typing-bubble{min-height:25px}.typing-dots{display:inline-flex;gap:4px;align-items:center}.typing-dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing-blink 1.2s ease-in-out infinite}.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}@keyframes typing-blink{0%,80%,to{opacity:.3}40%{opacity:1}}.typing-static{display:none;font-weight:700;letter-spacing:2px}.system-note{padding:2px 8px;color:var(--muted);text-align:center;font-size:12.5px;line-height:1.45}.sources-row{margin-top:-18px}.sources-card{width:100%}.sources-card summary{width:max-content;min-height:44px;display:flex;align-items:center;border-radius:7px;padding:5px 7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer}.sources-card[open] summary{margin-bottom:8px}.sources-list{display:flex;flex-wrap:wrap;gap:7px}.source-chip{min-height:44px;max-width:100%;display:inline-flex;flex-direction:column;justify-content:center;gap:1px;border-radius:8px;padding:7px 9px;color:inherit;background:var(--soft);text-decoration:none}.source-chip:hover{background:var(--accent-soft)}.source-chip span{color:var(--ink, #121212);font-size:12.5px;font-weight:650}.source-chip small{color:var(--muted);font-size:10.5px}.follow-up-prompts{margin-top:9px}.follow-up-prompts>p{margin:0 0 7px;color:var(--muted);font-size:12px;font-weight:650}.follow-up-list{display:flex;flex-wrap:wrap;gap:7px}.follow-up-chip{min-height:36px;border-radius:8px;padding:7px 10px;color:var(--ink, #121212);background:var(--soft);font-size:12px;font-weight:600;text-align:left}.follow-up-chip:hover{background:var(--accent-soft)}.bubble-row{display:flex;align-items:flex-start;gap:10px}.bubble-stack{width:100%;max-width:31rem;display:grid;gap:10px}.chat-card{display:grid;gap:11px;border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--card)}.chat-card-lead{margin:0;font-size:14px;font-weight:680}.chat-card-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.contact-field,.code-field{display:grid;gap:6px}.contact-field span,.code-field span{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.contact-field input,.code-field input{min-width:0;min-height:44px;border:1px solid var(--line-strong);border-radius:9px;padding:0 12px;color:var(--ink, #121212);background:#fff;font-size:16px}.code-field input{min-height:48px;font-size:20px;font-weight:750;letter-spacing:.24em;text-align:center}.contact-field input::placeholder{color:var(--muted)}.access-button{min-height:44px;border-radius:9px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.access-button:not(:disabled):hover{transform:translateY(-1px)}.access-feedback{color:var(--muted);font-size:11.5px;line-height:1.45}.session-turnstile,.verify-turnstile{overflow:hidden}.session-turnstile{position:fixed;z-index:30;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:min(20rem,calc(100% - 32px));pointer-events:none}.session-turnstile:not(:empty){min-height:65px;pointer-events:auto}.session-turnstile:empty,.verify-turnstile:empty{display:none}.cf-turnstile{min-height:65px;overflow:hidden}.verify-form{display:grid;gap:11px}.eyebrow{margin:0;color:var(--accent-text, var(--accent, #c87942));font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.ticket-card{gap:6px}.ticket-title{margin:0;font-size:15px;font-weight:700}.ticket-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.ticket-card dl{margin:4px 0 0;display:flex;gap:22px}.ticket-card dl div{display:grid;gap:3px}.ticket-card dt{color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.ticket-card dd{margin:0;font-size:13px;font-weight:700}.composer-bar{flex:none;border-top:1px solid var(--line);background:color-mix(in srgb,var(--card) 94%,transparent);backdrop-filter:blur(12px)}.composer-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:10px 0 max(8px,env(safe-area-inset-bottom))}.composer{display:flex;align-items:center;gap:8px}.composer input{flex:1 1 auto;min-width:0;min-height:46px;border:1px solid var(--line-strong);border-radius:12px;padding:0 14px;color:var(--ink, #121212);background:#fff;font-size:16px}.mic-button,.human-action-button{width:46px;height:46px;flex:none;display:grid;place-items:center;border:1px solid var(--line-strong);border-radius:12px;color:var(--ink, #121212);background:var(--card)}.mic-button--active{border-color:transparent;color:var(--accent-ink, #000);background:var(--accent, #c87942);animation:mic-pulse 1.6s ease-in-out infinite}@keyframes mic-pulse{50%{box-shadow:0 0 0 8px color-mix(in srgb,var(--accent, #c87942) 18%,transparent)}}.mute-button{min-height:44px;flex:none;border:1px solid var(--line-strong);border-radius:10px;padding:0 13px;color:var(--ink, #121212);background:var(--card);font-size:12.5px;font-weight:650}.send-button{min-height:46px;flex:none;border-radius:10px;padding:0 18px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.send-button:not(:disabled):hover,.mic-button:not(:disabled):hover,.human-action-button:not(:disabled):hover{transform:translateY(-1px)}@media(max-width:700px){.chat-header-inner{width:100%;min-height:58px;padding:0 14px}.brand-sub{display:none}.support-nav a,.text-button{min-height:44px;font-size:14px}.app[data-view=landing] .thread-inner{width:100%;padding:44px 20px 32px}.thread-inner,.composer-inner{width:100%;padding-left:16px;padding-right:16px}.landing-hero h1{font-size:2.25rem;line-height:1.05}.landing-lede{margin-top:16px;font-size:14px}.ask-composer{margin-top:24px;border-radius:18px}.landing-trust{font-size:11.5px}.topics-section{margin-top:52px}.topic-grid{grid-template-columns:1fr;gap:31px}.topics-heading>a,.topic-card h3 a,.topic-card li a{min-height:44px;display:flex;align-items:center}.topic-card ul{margin-top:4px;gap:0}.human-help-button{position:static;width:100%;margin-top:48px;justify-content:center;font-size:16px}.thread-inner{padding-top:24px}.thread-list{padding-bottom:28px}.bubble-stack{max-width:100%}.bubble-row>.avatar{display:none}.composer-inner{padding-top:8px;padding-bottom:max(16px,env(safe-area-inset-bottom))}.composer input{font-size:16px}.composer .mic-button:not(.mic-button--active){display:none}.send-button{min-width:54px;padding:0 12px}.follow-up-chip{min-height:44px;font-size:16px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.typing-dots{display:none}.typing-static{display:inline}} +:root{color-scheme:light;font-family:var(--font, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif);color:var(--ink, #121212);background:var(--canvas, #ffffff);font-synthesis:none;--card: #ffffff;--soft: color-mix(in srgb, var(--ink, #121212) 4%, #ffffff);--line: color-mix(in srgb, var(--ink, #121212) 12%, transparent);--line-strong: color-mix(in srgb, var(--ink, #121212) 22%, transparent);--muted: color-mix(in srgb, var(--ink, #121212) 62%, var(--canvas, #ffffff));--accent-soft: color-mix(in srgb, var(--accent, #c87942) 9%, #ffffff);--accent-line: color-mix(in srgb, var(--accent, #c87942) 34%, transparent);--focus-ring: color-mix(in srgb, var(--accent, #c87942) 50%, transparent);--surface-width: 64rem;--answer-width: 47.5rem}*{box-sizing:border-box}html,body{min-height:100%}html.support-conversation,body.support-conversation{height:100%;overflow:hidden;overscroll-behavior:none}body{margin:0;min-width:320px;font-family:inherit;background:var(--canvas, #fff)}button,input{font:inherit}button{border:0;cursor:pointer;touch-action:manipulation}button:disabled{cursor:not-allowed;opacity:.45}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.skip-link{position:fixed;z-index:20;top:8px;left:8px;transform:translateY(-150%);border-radius:8px;padding:10px 14px;color:var(--ink, #121212);background:#fff;box-shadow:0 2px 12px #0000001f;font-weight:700}.skip-link:focus{transform:translateY(0)}.app{min-height:100dvh;display:flex;flex-direction:column}.app[data-view=conversation]{position:fixed;z-index:1;top:var(--support-viewport-top, 0px);right:0;left:0;width:100%;min-height:0;height:var(--support-viewport-height, 100dvh);max-height:var(--support-viewport-height, 100dvh);overflow:hidden}.chat-header{flex:none;padding-top:env(safe-area-inset-top);background:var(--card)}.chat-header-inner{width:min(var(--surface-width),100% - 40px);min-height:64px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.header-actions,.support-nav{display:flex;align-items:center;gap:8px}.support-nav a,.text-button{min-height:44px;display:inline-flex;align-items:center;border-radius:9px;padding:0 9px;color:var(--muted);background:transparent;font-size:13px;font-weight:650;text-decoration:none;white-space:nowrap}.support-nav a:hover,.text-button:not(:disabled):hover{color:var(--ink, #121212);background:var(--soft)}.brand{min-width:0;display:inline-flex;align-items:center;gap:10px;color:inherit;text-decoration:none}.brand-logo{display:block;height:28px;max-width:150px;object-fit:contain}.brand-mark,.avatar{flex:none;display:grid;place-items:center;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-size:11px;font-weight:800}.brand-mark{width:32px;height:32px;border-radius:9px}.avatar{width:28px;height:28px;border-radius:50%}.brand-text{min-width:0;display:grid;line-height:1.25}.brand-name{overflow:hidden;font-size:15px;font-weight:720;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.brand-sub{color:var(--muted);font-size:12px}.reconnect-banner{flex:none;padding:8px 16px;border-bottom:1px solid var(--accent-line);color:var(--ink, #121212);background:var(--accent-soft);text-align:center;font-size:13px;font-weight:650}.thread{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-padding-block:24px;scrollbar-color:var(--line-strong) transparent}.thread-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:36px 0 24px}.app[data-view=landing] .thread{flex:none;overflow:visible}.app[data-view=landing] .thread-inner{width:min(var(--surface-width),100% - 40px);padding:104px 0 80px}.landing-hero h1{max-width:18ch;margin:0;font-size:clamp(2.5rem,4vw,3rem);font-weight:650;letter-spacing:-.045em;line-height:1}.landing-lede{max-width:42rem;margin:20px 0 0;color:var(--muted);font-size:15px;line-height:1.55}.ask-composer{width:100%;min-height:52px;margin-top:30px;display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:13px;padding:4px 5px 4px 10px;background:#fff;box-shadow:0 1px #0000000a,0 6px 18px #0000000f}.ask-composer:focus-within{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--focus-ring),0 8px 24px #00000014}.ask-icon{flex:none;display:grid;place-items:center}.ask-icon .avatar{width:24px;height:24px;font-size:9px}.ask-composer input{flex:1 1 auto;min-width:0;min-height:44px;border:0;padding:0 3px;color:var(--ink, #121212);background:transparent;font-size:16px;outline:0}.ask-composer input::placeholder,.composer input::placeholder{color:var(--muted)}.ask-composer input:focus-visible{outline:0}.ask-composer .mic-button,.ask-send-button{width:44px;height:44px;min-height:44px;padding:0}.ask-composer .mic-button{border:0}.ask-send-button{border-radius:9px;display:grid;place-items:center}.landing-trust{margin:13px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.landing-status{min-height:18px;margin:6px 0 0;color:var(--ink, #121212);font-size:12px;font-weight:650;line-height:1.5}.topics-section{margin-top:72px}.topics-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.topics-heading h2{margin:0;font-size:21px;font-weight:670;letter-spacing:-.025em}.topics-heading>a{color:var(--muted);font-size:13px;font-weight:650}.topic-grid{margin-top:27px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));column-gap:56px;row-gap:42px}.topic-card h3{margin:0;font-size:14px;font-weight:720;line-height:1.35}.topic-card h3 a,.topic-card li a{color:var(--ink, #121212);text-decoration:none}.topic-card h3 a:hover,.topic-card li a:hover{text-decoration:underline;text-underline-offset:3px}.topic-card>p{margin:7px 0 0;color:var(--muted);font-size:12.5px;line-height:1.5}.topic-card ul{margin:12px 0 0;padding:0;list-style:none;display:grid;gap:8px}.topic-card li{font-size:13px;line-height:1.45}.topics-empty{grid-column:1 / -1;margin:0;color:var(--muted)}.human-help-button{position:fixed;z-index:5;right:18px;bottom:max(16px,env(safe-area-inset-bottom));min-height:48px;display:inline-flex;align-items:center;gap:9px;border:1px solid var(--line-strong);border-radius:12px;padding:0 15px 0 10px;color:var(--ink, #121212);background:#fff;box-shadow:0 4px 16px #00000017;font-size:13px;font-weight:700}.human-help-button:hover{transform:translateY(-1px);box-shadow:0 6px 20px #0000001f}.human-help-button .avatar{width:26px;height:26px;font-size:9px}.human-help-icon{display:grid;place-items:center}.thread-list{margin:0;padding:0 0 36px;list-style:none;display:flex;flex-direction:column;gap:28px}.answer-turn{display:grid;gap:8px}.answer-turn--user{width:100%}.turn-label{margin:0;color:var(--muted);font-size:12px;font-weight:700}.turn-copy{color:var(--ink, #121212);font-size:15px;line-height:1.62}.turn-copy p{margin:0;overflow-wrap:anywhere}.answer-turn--user .turn-copy{font-size:clamp(1.35rem,4vw,1.5rem);font-weight:650;letter-spacing:-.025em;line-height:1.25}.answer-turn--user .turn-copy h2{margin:0;overflow-wrap:anywhere;font:inherit}.answer-turn--pending{opacity:.65}.typing-bubble{min-height:25px}.typing-dots{display:inline-flex;gap:4px;align-items:center}.typing-dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing-blink 1.2s ease-in-out infinite}.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}@keyframes typing-blink{0%,80%,to{opacity:.3}40%{opacity:1}}.typing-static{display:none;font-weight:700;letter-spacing:2px}.system-note{padding:2px 8px;color:var(--muted);text-align:center;font-size:12.5px;line-height:1.45}.sources-row{margin-top:-18px}.sources-card{width:100%}.sources-card summary{width:max-content;min-height:44px;display:flex;align-items:center;border-radius:7px;padding:5px 7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer}.sources-card[open] summary{margin-bottom:8px}.sources-list{display:flex;flex-wrap:wrap;gap:7px}.source-chip{min-height:44px;max-width:100%;display:inline-flex;flex-direction:column;justify-content:center;gap:1px;border-radius:8px;padding:7px 9px;color:inherit;background:var(--soft);text-decoration:none}.source-chip:hover{background:var(--accent-soft)}.source-chip span{color:var(--ink, #121212);font-size:12.5px;font-weight:650}.source-chip small{color:var(--muted);font-size:10.5px}.follow-up-prompts{margin-top:9px}.follow-up-prompts>p{margin:0 0 7px;color:var(--muted);font-size:12px;font-weight:650}.follow-up-list{display:flex;flex-wrap:wrap;gap:7px}.follow-up-chip{min-height:36px;border-radius:8px;padding:7px 10px;color:var(--ink, #121212);background:var(--soft);font-size:12px;font-weight:600;text-align:left}.follow-up-chip:hover{background:var(--accent-soft)}.bubble-row{display:flex;align-items:flex-start;gap:10px}.bubble-stack{width:100%;max-width:31rem;display:grid;gap:10px}.chat-card{display:grid;gap:11px;border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--card)}.chat-card-lead{margin:0;font-size:14px;font-weight:680}.chat-card-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.contact-field,.code-field{display:grid;gap:6px}.contact-field span,.code-field span{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.contact-field input,.code-field input{min-width:0;min-height:44px;border:1px solid var(--line-strong);border-radius:9px;padding:0 12px;color:var(--ink, #121212);background:#fff;font-size:16px}.code-field input{min-height:48px;font-size:20px;font-weight:750;letter-spacing:.24em;text-align:center}.contact-field input::placeholder{color:var(--muted)}.access-button{min-height:44px;border-radius:9px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.access-button:not(:disabled):hover{transform:translateY(-1px)}.access-feedback{color:var(--muted);font-size:11.5px;line-height:1.45}.session-turnstile,.verify-turnstile{overflow:hidden}.session-turnstile{position:fixed;z-index:30;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:min(20rem,calc(100% - 32px));pointer-events:none}.session-turnstile:not(:empty){min-height:65px;pointer-events:auto}.session-turnstile:empty,.verify-turnstile:empty{display:none}.cf-turnstile{min-height:65px;overflow:hidden}.verify-form{display:grid;gap:11px}.eyebrow{margin:0;color:var(--accent-text, var(--accent, #c87942));font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.ticket-card{gap:6px}.ticket-title{margin:0;font-size:15px;font-weight:700}.ticket-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.ticket-card dl{margin:4px 0 0;display:flex;gap:22px}.ticket-card dl div{display:grid;gap:3px}.ticket-card dt{color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.ticket-card dd{margin:0;font-size:13px;font-weight:700}.composer-bar{flex:none;border-top:1px solid var(--line);background:color-mix(in srgb,var(--card) 94%,transparent);backdrop-filter:blur(12px)}.composer-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:10px 0 max(8px,env(safe-area-inset-bottom))}.composer{display:flex;align-items:center;gap:8px}.composer input{flex:1 1 auto;min-width:0;min-height:46px;border:1px solid var(--line-strong);border-radius:12px;padding:0 14px;color:var(--ink, #121212);background:#fff;font-size:16px}.mic-button,.human-action-button{width:46px;height:46px;flex:none;display:grid;place-items:center;border:1px solid var(--line-strong);border-radius:12px;color:var(--ink, #121212);background:var(--card)}.mic-button--active{border-color:transparent;color:var(--accent-ink, #000);background:var(--accent, #c87942);animation:mic-pulse 1.6s ease-in-out infinite}@keyframes mic-pulse{50%{box-shadow:0 0 0 8px color-mix(in srgb,var(--accent, #c87942) 18%,transparent)}}.mute-button{min-height:44px;flex:none;border:1px solid var(--line-strong);border-radius:10px;padding:0 13px;color:var(--ink, #121212);background:var(--card);font-size:12.5px;font-weight:650}.send-button{min-height:46px;flex:none;border-radius:10px;padding:0 18px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.send-button:not(:disabled):hover,.mic-button:not(:disabled):hover,.human-action-button:not(:disabled):hover{transform:translateY(-1px)}@media(max-width:700px){.chat-header-inner{width:100%;min-height:58px;padding:0 14px}.brand-sub{display:none}.support-nav a,.text-button{min-height:44px;font-size:14px}.app[data-view=landing] .thread-inner{width:100%;padding:44px 20px calc(132px + env(safe-area-inset-bottom))}.thread-inner,.composer-inner{width:100%;padding-left:16px;padding-right:16px}.landing-hero h1{font-size:2.25rem;line-height:1.05}.landing-lede{margin-top:16px;font-size:14px}.ask-composer{position:fixed;z-index:10;right:76px;bottom:max(16px,env(safe-area-inset-bottom));left:16px;width:auto;min-height:54px;margin:0;border-radius:27px}.landing-trust{font-size:11.5px}.topics-section{margin-top:52px}.topic-grid{grid-template-columns:1fr;gap:31px}.topics-heading>a,.topic-card h3 a,.topic-card li a{min-height:44px;display:flex;align-items:center}.topic-card ul{margin-top:4px;gap:0}.human-help-button{position:fixed;z-index:10;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:48px;min-height:48px;justify-content:center;margin:0;padding:0;border-radius:50%;font-size:16px}.human-help-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.thread-inner{padding-top:24px}.thread-list{padding-bottom:28px}.bubble-stack{max-width:100%}.bubble-row>.avatar{display:none}.composer-inner{padding-top:8px;padding-bottom:max(16px,env(safe-area-inset-bottom))}.composer input{font-size:16px}.composer .mic-button:not(.mic-button--active){display:none}.send-button{min-width:54px;padding:0 12px}.follow-up-chip{min-height:44px;font-size:16px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.typing-dots{display:none}.typing-static{display:inline}} diff --git a/apps/desk/src/voice/contact.ts b/apps/desk/src/voice/contact.ts index 2ac2edb..2496c42 100644 --- a/apps/desk/src/voice/contact.ts +++ b/apps/desk/src/voice/contact.ts @@ -32,6 +32,8 @@ export type VoiceContactContinuation = export const PRODUCT_HELP_CONTACT_CONTINUATION = 'I have shared my name and email. Ask me for my order number before opening a ticket.' +export const ORDER_LOOKUP_CONTACT_CONTINUATION = + 'I have shared my name and email. Continue the order lookup using the order number I already gave you, or ask me for it if it is missing.' /** * Resume the exact agent flow interrupted by the in-thread identity card. These @@ -45,7 +47,7 @@ export function contactContinuationMessage(value: unknown): string | null { case 'open_ticket': return 'I have shared my name and email. Continue opening the support ticket I requested.' case 'order_lookup': - return 'I have shared my name and email. Continue the order lookup using the order number I already gave you, or ask me for it if it is missing.' + return ORDER_LOOKUP_CONTACT_CONTINUATION case 'product_help': return PRODUCT_HELP_CONTACT_CONTINUATION default: diff --git a/apps/desk/src/voice/conversation.ts b/apps/desk/src/voice/conversation.ts index 7fe689b..af64f1f 100644 --- a/apps/desk/src/voice/conversation.ts +++ b/apps/desk/src/voice/conversation.ts @@ -1,4 +1,7 @@ -import { PRODUCT_HELP_CONTACT_CONTINUATION } from './contact' +import { + ORDER_LOOKUP_CONTACT_CONTINUATION, + PRODUCT_HELP_CONTACT_CONTINUATION, +} from './contact' export const VOICE_AGENT_MODEL = '@cf/zai-org/glm-4.7-flash' @@ -26,7 +29,10 @@ function isSupportContinuation( } export function directVoiceResponse(transcript: string, messages: VoiceModelMessage[] = []): string | null { - if (transcript.trim() === PRODUCT_HELP_CONTACT_CONTINUATION) { + if ( + transcript.trim() === PRODUCT_HELP_CONTACT_CONTINUATION + || transcript.trim() === ORDER_LOOKUP_CONTACT_CONTINUATION + ) { const orderNumberAlreadyShared = messages .slice(0, -1) .some((message) => message.role === 'user' && ORDER_NUMBER_MENTION.test(message.content)) diff --git a/apps/desk/src/voice/demo-client.ts b/apps/desk/src/voice/demo-client.ts index daf3338..695cd9c 100644 --- a/apps/desk/src/voice/demo-client.ts +++ b/apps/desk/src/voice/demo-client.ts @@ -51,19 +51,12 @@ const elements = { verifyFeedback: required('#verify-feedback'), } -const SESSION_KEY = 'morrow-voice-support-session' const RESET_FOCUS_KEY = 'morrow-voice-support-reset-focus' function sessionName(): string { - try { - const existing = sessionStorage.getItem(SESSION_KEY) - if (existing) return existing - const created = `voice-${crypto.randomUUID().replaceAll('-', '').slice(0, 20)}` - sessionStorage.setItem(SESSION_KEY, created) - return created - } catch { - return `voice-${crypto.randomUUID().replaceAll('-', '').slice(0, 20)}` - } + // Help always opens at its landing page. A browser refresh is a new visit, + // not an accidental restoration of a previous transcript. + return `voice-${crypto.randomUUID().replaceAll('-', '').slice(0, 20)}` } const client = new VoiceClient({ @@ -82,6 +75,7 @@ let contactPending = false type SourceArticle = { title: string; section: string; url: string } let latestMessages: TranscriptMessage[] = [] +const hiddenTranscriptMessages = new Set() let notes: { anchor: number; text: string }[] = [] let sources: { anchor: number; articles: SourceArticle[] }[] = [] let ticketAnchor: number | null = null @@ -125,7 +119,6 @@ function isReady(): boolean { function reloadFreshSession(): void { try { - sessionStorage.removeItem(SESSION_KEY) sessionStorage.setItem(RESET_FOCUS_KEY, '1') } catch { // A reload still clears the VoiceClient's in-memory transcript when @@ -331,7 +324,10 @@ function renderThread(): void { } for (let index = 0; index < length; index++) { pushExtras(index) - rows.push(messageRow(latestMessages[index]!)) + const message = latestMessages[index]! + if (!(message.role === 'user' && hiddenTranscriptMessages.has(message.text))) { + rows.push(messageRow(message)) + } } pushExtras(length) if (interimText) rows.push(pendingRow(interimText)) @@ -618,10 +614,13 @@ function renderCustomMessage(value: unknown): void { const contact = message.contact as { email?: unknown } | undefined const email = typeof contact?.email === 'string' ? contact.email : 'your email' pushNote(`We’ll use ${email} for this support action`) - // Mid-conversation the card interrupted a flow Ava offered to continue; - // a visible, intent-specific confirmation hands the exact flow back to her. + // This is a machine-readable continuation, not customer copy. It lets Ava + // resume the interrupted flow while keeping the transcript natural. const continuation = contactContinuationMessage(message.continuation) - if (latestMessages.length > 0 && continuation) client.sendText(continuation) + if (latestMessages.length > 0 && continuation) { + hiddenTranscriptMessages.add(continuation) + client.sendText(continuation) + } else elements.textInput.focus() return } diff --git a/apps/desk/src/voice/demo-page.ts b/apps/desk/src/voice/demo-page.ts index 59dc783..8cdc02c 100644 --- a/apps/desk/src/voice/demo-page.ts +++ b/apps/desk/src/voice/demo-page.ts @@ -159,8 +159,8 @@ export function voiceDemoPageResponse( @@ -225,7 +225,7 @@ export function voiceDemoPageResponse( - diff --git a/apps/desk/src/voice/demo.css b/apps/desk/src/voice/demo.css index b25008d..da66a48 100644 --- a/apps/desk/src/voice/demo.css +++ b/apps/desk/src/voice/demo.css @@ -196,6 +196,7 @@ button:focus-visible, input:focus-visible, a:focus-visible, summary:focus-visibl } .human-help-button:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(0,0,0,.12); } .human-help-button .avatar { width: 26px; height: 26px; font-size: 9px; } +.human-help-icon { display: grid; place-items: center; } .thread-list { margin: 0; padding: 0 0 36px; list-style: none; display: flex; flex-direction: column; gap: 28px; } .answer-turn { display: grid; gap: 8px; } @@ -281,17 +282,40 @@ button:focus-visible, input:focus-visible, a:focus-visible, summary:focus-visibl .chat-header-inner { width: 100%; min-height: 58px; padding: 0 14px; } .brand-sub { display: none; } .support-nav a, .text-button { min-height: 44px; font-size: 14px; } - .app[data-view="landing"] .thread-inner { width: 100%; padding: 44px 20px 32px; } + .app[data-view="landing"] .thread-inner { width: 100%; padding: 44px 20px calc(132px + env(safe-area-inset-bottom)); } .thread-inner, .composer-inner { width: 100%; padding-left: 16px; padding-right: 16px; } .landing-hero h1 { font-size: 2.25rem; line-height: 1.05; } .landing-lede { margin-top: 16px; font-size: 14px; } - .ask-composer { margin-top: 24px; border-radius: 18px; } + .ask-composer { + position: fixed; + z-index: 10; + right: 76px; + bottom: max(16px, env(safe-area-inset-bottom)); + left: 16px; + width: auto; + min-height: 54px; + margin: 0; + border-radius: 27px; + } .landing-trust { font-size: 11.5px; } .topics-section { margin-top: 52px; } .topic-grid { grid-template-columns: 1fr; gap: 31px; } .topics-heading > a, .topic-card h3 a, .topic-card li a { min-height: 44px; display: flex; align-items: center; } .topic-card ul { margin-top: 4px; gap: 0; } - .human-help-button { position: static; width: 100%; margin-top: 48px; justify-content: center; font-size: 16px; } + .human-help-button { + position: fixed; + z-index: 10; + right: 16px; + bottom: max(16px, env(safe-area-inset-bottom)); + width: 48px; + min-height: 48px; + justify-content: center; + margin: 0; + padding: 0; + border-radius: 50%; + font-size: 16px; + } + .human-help-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .thread-inner { padding-top: 24px; } .thread-list { padding-bottom: 28px; } .bubble-stack { max-width: 100%; } diff --git a/apps/desk/test/visual/portal.visual.spec.ts b/apps/desk/test/visual/portal.visual.spec.ts index 1745f29..9c9543c 100644 --- a/apps/desk/test/visual/portal.visual.spec.ts +++ b/apps/desk/test/visual/portal.visual.spec.ts @@ -144,7 +144,10 @@ type VoiceSocketFixture = { async function installVoiceSocketFixture( page: Page, - options: { holdSessionReady?: boolean } = {}, + options: { + holdSessionReady?: boolean + contactContinuation?: 'order_lookup' | 'open_ticket' | 'product_help' + } = {}, ): Promise { let socket: WebSocketRoute | null = null let readyPending = false @@ -172,12 +175,23 @@ async function installVoiceSocketFixture( else readyPending = true return } + if (parsed.type === 'set_voice_contact') { + send({ + type: 'voice_contact_set', + contact: { name: parsed.name, email: parsed.email }, + continuation: options.contactContinuation ?? 'continue', + }) + return + } if (parsed.type !== 'text_message' || typeof parsed.text !== 'string') return turn += 1 const answer = `Deterministic support answer ${turn}. Keep the machine unplugged while checking the removable parts. Use the published care instructions below, and stop if anything looks damaged.` send({ type: 'status', status: 'thinking' }) send({ type: 'transcript', role: 'user', text: parsed.text }) + if (turn === 1 && options.contactContinuation) { + send({ type: 'voice_contact_required', anchor: 'after_reply' }) + } send({ type: 'voice_sources', articles: [{ @@ -250,20 +264,44 @@ test('support home opens with the agent as the primary help experience', async ( await expect(page.getByRole('search')).toHaveCount(0) await expect(page.getByRole('link', { name: 'Browse help' })).toHaveAttribute('href', '/kb') await expect(page.getByRole('heading', { level: 2, name: 'Browse by topic' })).toBeVisible() - await expect(page.getByRole('button', { name: 'Chat with a human' })).toBeEnabled() + await expect(page.getByRole('button', { name: 'Ask the team' })).toBeEnabled() const composerPosition = await page.locator('.ask-composer').evaluate((element) => { const box = element.getBoundingClientRect() - return { top: box.top, viewportHeight: window.innerHeight } + return { top: box.top, bottom: box.bottom, viewportHeight: window.innerHeight } }) - expect(composerPosition.top).toBeLessThan(composerPosition.viewportHeight * .55) const width = viewportWidth(testInfo) + if (width <= 700) { + expect(composerPosition.top).toBeGreaterThan(composerPosition.viewportHeight * .65) + expect(composerPosition.bottom).toBeLessThanOrEqual(composerPosition.viewportHeight - 12) + } else { + expect(composerPosition.top).toBeLessThan(composerPosition.viewportHeight * .55) + } await expectAcceptanceBasics(page, width) await expectMobileTargets(page, width, '.support-nav a, .topics-heading a, .topic-card a') await screenshot(page, 'agent-home.png') }) +test('contact continuation stays out of the customer transcript', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'mobile-390', 'The mobile contact handoff is covered once.') + await installVoiceSocketFixture(page, { contactContinuation: 'order_lookup' }) + await page.goto('/', { waitUntil: 'networkidle' }) + + await page.getByLabel('Ask anything').fill('Where is my order?') + await page.getByRole('button', { name: 'Send question' }).click() + await expect(page.getByLabel('Name')).toBeVisible() + await expect(page.getByText('Deterministic support answer 1.', { exact: false })).toBeVisible() + + await page.getByLabel('Name').fill('Avery Customer') + await page.getByLabel('Email').fill('avery@example.test') + await page.getByRole('button', { name: 'Continue' }).click() + + await expect(page.getByText('We’ll use avery@example.test for this support action')).toBeVisible() + await expect(page.getByText('Deterministic support answer 2.', { exact: false })).toBeVisible() + await expect(page.getByText('I have shared my name and email.', { exact: false })).toHaveCount(0) +}) + test('mobile landing remains stable while the agent session becomes ready', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'mobile-390', 'The delayed readiness lifecycle is covered at one phone viewport.') const fixture = await installVoiceSocketFixture(page, { holdSessionReady: true }) diff --git a/apps/desk/test/voice-conversation.test.ts b/apps/desk/test/voice-conversation.test.ts index d91bf9a..6ba54b0 100644 --- a/apps/desk/test/voice-conversation.test.ts +++ b/apps/desk/test/voice-conversation.test.ts @@ -4,6 +4,7 @@ import { prepareVoiceModelMessages, voiceAgentSystemPrompt, } from '../src/voice/conversation' +import { ORDER_LOOKUP_CONTACT_CONTINUATION } from '../src/voice/contact' describe('voice conversation policy', () => { it('joins a spoken ticket request split at a natural pause', () => { @@ -45,6 +46,17 @@ describe('voice conversation policy', () => { ])).toBeNull() }) + it('continues a direct order lookup after the contact card without asking twice', () => { + expect(directVoiceResponse(ORDER_LOOKUP_CONTACT_CONTINUATION)).toBe( + 'What is the order number from your confirmation email?', + ) + expect(directVoiceResponse(ORDER_LOOKUP_CONTACT_CONTINUATION, [ + { role: 'user', content: 'Where is order #FC-4021?' }, + { role: 'assistant', content: 'Add your name and email in the card below.' }, + { role: 'user', content: ORDER_LOOKUP_CONTACT_CONTINUATION }, + ])).toBeNull() + }) + it('never promises order lookup when Shopify is unavailable', () => { const anonymous = voiceAgentSystemPrompt('Example Company', { orders: false, contact: false }) expect(anonymous).toContain('Order lookup is not available in this workspace.') diff --git a/apps/desk/test/voice-demo.test.ts b/apps/desk/test/voice-demo.test.ts index 35d8009..ce7b51b 100644 --- a/apps/desk/test/voice-demo.test.ts +++ b/apps/desk/test/voice-demo.test.ts @@ -117,6 +117,7 @@ describe('voice demo boundary', () => { expect(html).toContain('href="/kb">Browse help') expect(html).toContain('id="human-help-button"') expect(html).toContain('id="conversation-human-button"') + expect(html).toContain('aria-label="Ask the team"') expect(html).toContain('id="reconnect-banner"') expect(html).not.toContain('class="privacy-note"') expect(html).toContain('viewport-fit=cover') From eed880c7f0b843401ec521b7b9e6a94ffc406e8c Mon Sep 17 00:00:00 2001 From: Morrow Contributors Date: Thu, 6 Aug 2026 20:33:14 +0800 Subject: [PATCH 2/6] feat: route anonymous order questions into the contact card Ported from AbleDesk PR #48 (baaac44). An order question from an unidentified visitor is routed deterministically into the contact card instead of relying on the model to ask for identity, and the Shopify order-lookup bindings are wired into the local and test runtimes. Binding and environment variable names keep the MORROW_ prefix. --- apps/desk/public/voice-demo.css | 2 +- apps/desk/scripts/visual-server.mjs | 7 ++++ apps/desk/src/voice/demo-agent.ts | 29 ++++++++++++++- apps/desk/src/voice/demo.css | 2 +- apps/desk/src/voice/orders.ts | 41 +++++++++++++++++++++ apps/desk/test/visual/portal.visual.spec.ts | 11 ++++++ apps/desk/test/voice-agent.test.ts | 32 ++++++++++++++++ apps/desk/test/voice-orders.test.ts | 34 ++++++++++++++++- apps/desk/vitest.config.ts | 2 + 9 files changed, 155 insertions(+), 5 deletions(-) diff --git a/apps/desk/public/voice-demo.css b/apps/desk/public/voice-demo.css index fda5334..2b6ee06 100644 --- a/apps/desk/public/voice-demo.css +++ b/apps/desk/public/voice-demo.css @@ -1 +1 @@ -:root{color-scheme:light;font-family:var(--font, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif);color:var(--ink, #121212);background:var(--canvas, #ffffff);font-synthesis:none;--card: #ffffff;--soft: color-mix(in srgb, var(--ink, #121212) 4%, #ffffff);--line: color-mix(in srgb, var(--ink, #121212) 12%, transparent);--line-strong: color-mix(in srgb, var(--ink, #121212) 22%, transparent);--muted: color-mix(in srgb, var(--ink, #121212) 62%, var(--canvas, #ffffff));--accent-soft: color-mix(in srgb, var(--accent, #c87942) 9%, #ffffff);--accent-line: color-mix(in srgb, var(--accent, #c87942) 34%, transparent);--focus-ring: color-mix(in srgb, var(--accent, #c87942) 50%, transparent);--surface-width: 64rem;--answer-width: 47.5rem}*{box-sizing:border-box}html,body{min-height:100%}html.support-conversation,body.support-conversation{height:100%;overflow:hidden;overscroll-behavior:none}body{margin:0;min-width:320px;font-family:inherit;background:var(--canvas, #fff)}button,input{font:inherit}button{border:0;cursor:pointer;touch-action:manipulation}button:disabled{cursor:not-allowed;opacity:.45}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.skip-link{position:fixed;z-index:20;top:8px;left:8px;transform:translateY(-150%);border-radius:8px;padding:10px 14px;color:var(--ink, #121212);background:#fff;box-shadow:0 2px 12px #0000001f;font-weight:700}.skip-link:focus{transform:translateY(0)}.app{min-height:100dvh;display:flex;flex-direction:column}.app[data-view=conversation]{position:fixed;z-index:1;top:var(--support-viewport-top, 0px);right:0;left:0;width:100%;min-height:0;height:var(--support-viewport-height, 100dvh);max-height:var(--support-viewport-height, 100dvh);overflow:hidden}.chat-header{flex:none;padding-top:env(safe-area-inset-top);background:var(--card)}.chat-header-inner{width:min(var(--surface-width),100% - 40px);min-height:64px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.header-actions,.support-nav{display:flex;align-items:center;gap:8px}.support-nav a,.text-button{min-height:44px;display:inline-flex;align-items:center;border-radius:9px;padding:0 9px;color:var(--muted);background:transparent;font-size:13px;font-weight:650;text-decoration:none;white-space:nowrap}.support-nav a:hover,.text-button:not(:disabled):hover{color:var(--ink, #121212);background:var(--soft)}.brand{min-width:0;display:inline-flex;align-items:center;gap:10px;color:inherit;text-decoration:none}.brand-logo{display:block;height:28px;max-width:150px;object-fit:contain}.brand-mark,.avatar{flex:none;display:grid;place-items:center;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-size:11px;font-weight:800}.brand-mark{width:32px;height:32px;border-radius:9px}.avatar{width:28px;height:28px;border-radius:50%}.brand-text{min-width:0;display:grid;line-height:1.25}.brand-name{overflow:hidden;font-size:15px;font-weight:720;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.brand-sub{color:var(--muted);font-size:12px}.reconnect-banner{flex:none;padding:8px 16px;border-bottom:1px solid var(--accent-line);color:var(--ink, #121212);background:var(--accent-soft);text-align:center;font-size:13px;font-weight:650}.thread{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-padding-block:24px;scrollbar-color:var(--line-strong) transparent}.thread-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:36px 0 24px}.app[data-view=landing] .thread{flex:none;overflow:visible}.app[data-view=landing] .thread-inner{width:min(var(--surface-width),100% - 40px);padding:104px 0 80px}.landing-hero h1{max-width:18ch;margin:0;font-size:clamp(2.5rem,4vw,3rem);font-weight:650;letter-spacing:-.045em;line-height:1}.landing-lede{max-width:42rem;margin:20px 0 0;color:var(--muted);font-size:15px;line-height:1.55}.ask-composer{width:100%;min-height:52px;margin-top:30px;display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:13px;padding:4px 5px 4px 10px;background:#fff;box-shadow:0 1px #0000000a,0 6px 18px #0000000f}.ask-composer:focus-within{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--focus-ring),0 8px 24px #00000014}.ask-icon{flex:none;display:grid;place-items:center}.ask-icon .avatar{width:24px;height:24px;font-size:9px}.ask-composer input{flex:1 1 auto;min-width:0;min-height:44px;border:0;padding:0 3px;color:var(--ink, #121212);background:transparent;font-size:16px;outline:0}.ask-composer input::placeholder,.composer input::placeholder{color:var(--muted)}.ask-composer input:focus-visible{outline:0}.ask-composer .mic-button,.ask-send-button{width:44px;height:44px;min-height:44px;padding:0}.ask-composer .mic-button{border:0}.ask-send-button{border-radius:9px;display:grid;place-items:center}.landing-trust{margin:13px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.landing-status{min-height:18px;margin:6px 0 0;color:var(--ink, #121212);font-size:12px;font-weight:650;line-height:1.5}.topics-section{margin-top:72px}.topics-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.topics-heading h2{margin:0;font-size:21px;font-weight:670;letter-spacing:-.025em}.topics-heading>a{color:var(--muted);font-size:13px;font-weight:650}.topic-grid{margin-top:27px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));column-gap:56px;row-gap:42px}.topic-card h3{margin:0;font-size:14px;font-weight:720;line-height:1.35}.topic-card h3 a,.topic-card li a{color:var(--ink, #121212);text-decoration:none}.topic-card h3 a:hover,.topic-card li a:hover{text-decoration:underline;text-underline-offset:3px}.topic-card>p{margin:7px 0 0;color:var(--muted);font-size:12.5px;line-height:1.5}.topic-card ul{margin:12px 0 0;padding:0;list-style:none;display:grid;gap:8px}.topic-card li{font-size:13px;line-height:1.45}.topics-empty{grid-column:1 / -1;margin:0;color:var(--muted)}.human-help-button{position:fixed;z-index:5;right:18px;bottom:max(16px,env(safe-area-inset-bottom));min-height:48px;display:inline-flex;align-items:center;gap:9px;border:1px solid var(--line-strong);border-radius:12px;padding:0 15px 0 10px;color:var(--ink, #121212);background:#fff;box-shadow:0 4px 16px #00000017;font-size:13px;font-weight:700}.human-help-button:hover{transform:translateY(-1px);box-shadow:0 6px 20px #0000001f}.human-help-button .avatar{width:26px;height:26px;font-size:9px}.human-help-icon{display:grid;place-items:center}.thread-list{margin:0;padding:0 0 36px;list-style:none;display:flex;flex-direction:column;gap:28px}.answer-turn{display:grid;gap:8px}.answer-turn--user{width:100%}.turn-label{margin:0;color:var(--muted);font-size:12px;font-weight:700}.turn-copy{color:var(--ink, #121212);font-size:15px;line-height:1.62}.turn-copy p{margin:0;overflow-wrap:anywhere}.answer-turn--user .turn-copy{font-size:clamp(1.35rem,4vw,1.5rem);font-weight:650;letter-spacing:-.025em;line-height:1.25}.answer-turn--user .turn-copy h2{margin:0;overflow-wrap:anywhere;font:inherit}.answer-turn--pending{opacity:.65}.typing-bubble{min-height:25px}.typing-dots{display:inline-flex;gap:4px;align-items:center}.typing-dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing-blink 1.2s ease-in-out infinite}.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}@keyframes typing-blink{0%,80%,to{opacity:.3}40%{opacity:1}}.typing-static{display:none;font-weight:700;letter-spacing:2px}.system-note{padding:2px 8px;color:var(--muted);text-align:center;font-size:12.5px;line-height:1.45}.sources-row{margin-top:-18px}.sources-card{width:100%}.sources-card summary{width:max-content;min-height:44px;display:flex;align-items:center;border-radius:7px;padding:5px 7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer}.sources-card[open] summary{margin-bottom:8px}.sources-list{display:flex;flex-wrap:wrap;gap:7px}.source-chip{min-height:44px;max-width:100%;display:inline-flex;flex-direction:column;justify-content:center;gap:1px;border-radius:8px;padding:7px 9px;color:inherit;background:var(--soft);text-decoration:none}.source-chip:hover{background:var(--accent-soft)}.source-chip span{color:var(--ink, #121212);font-size:12.5px;font-weight:650}.source-chip small{color:var(--muted);font-size:10.5px}.follow-up-prompts{margin-top:9px}.follow-up-prompts>p{margin:0 0 7px;color:var(--muted);font-size:12px;font-weight:650}.follow-up-list{display:flex;flex-wrap:wrap;gap:7px}.follow-up-chip{min-height:36px;border-radius:8px;padding:7px 10px;color:var(--ink, #121212);background:var(--soft);font-size:12px;font-weight:600;text-align:left}.follow-up-chip:hover{background:var(--accent-soft)}.bubble-row{display:flex;align-items:flex-start;gap:10px}.bubble-stack{width:100%;max-width:31rem;display:grid;gap:10px}.chat-card{display:grid;gap:11px;border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--card)}.chat-card-lead{margin:0;font-size:14px;font-weight:680}.chat-card-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.contact-field,.code-field{display:grid;gap:6px}.contact-field span,.code-field span{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.contact-field input,.code-field input{min-width:0;min-height:44px;border:1px solid var(--line-strong);border-radius:9px;padding:0 12px;color:var(--ink, #121212);background:#fff;font-size:16px}.code-field input{min-height:48px;font-size:20px;font-weight:750;letter-spacing:.24em;text-align:center}.contact-field input::placeholder{color:var(--muted)}.access-button{min-height:44px;border-radius:9px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.access-button:not(:disabled):hover{transform:translateY(-1px)}.access-feedback{color:var(--muted);font-size:11.5px;line-height:1.45}.session-turnstile,.verify-turnstile{overflow:hidden}.session-turnstile{position:fixed;z-index:30;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:min(20rem,calc(100% - 32px));pointer-events:none}.session-turnstile:not(:empty){min-height:65px;pointer-events:auto}.session-turnstile:empty,.verify-turnstile:empty{display:none}.cf-turnstile{min-height:65px;overflow:hidden}.verify-form{display:grid;gap:11px}.eyebrow{margin:0;color:var(--accent-text, var(--accent, #c87942));font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.ticket-card{gap:6px}.ticket-title{margin:0;font-size:15px;font-weight:700}.ticket-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.ticket-card dl{margin:4px 0 0;display:flex;gap:22px}.ticket-card dl div{display:grid;gap:3px}.ticket-card dt{color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.ticket-card dd{margin:0;font-size:13px;font-weight:700}.composer-bar{flex:none;border-top:1px solid var(--line);background:color-mix(in srgb,var(--card) 94%,transparent);backdrop-filter:blur(12px)}.composer-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:10px 0 max(8px,env(safe-area-inset-bottom))}.composer{display:flex;align-items:center;gap:8px}.composer input{flex:1 1 auto;min-width:0;min-height:46px;border:1px solid var(--line-strong);border-radius:12px;padding:0 14px;color:var(--ink, #121212);background:#fff;font-size:16px}.mic-button,.human-action-button{width:46px;height:46px;flex:none;display:grid;place-items:center;border:1px solid var(--line-strong);border-radius:12px;color:var(--ink, #121212);background:var(--card)}.mic-button--active{border-color:transparent;color:var(--accent-ink, #000);background:var(--accent, #c87942);animation:mic-pulse 1.6s ease-in-out infinite}@keyframes mic-pulse{50%{box-shadow:0 0 0 8px color-mix(in srgb,var(--accent, #c87942) 18%,transparent)}}.mute-button{min-height:44px;flex:none;border:1px solid var(--line-strong);border-radius:10px;padding:0 13px;color:var(--ink, #121212);background:var(--card);font-size:12.5px;font-weight:650}.send-button{min-height:46px;flex:none;border-radius:10px;padding:0 18px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.send-button:not(:disabled):hover,.mic-button:not(:disabled):hover,.human-action-button:not(:disabled):hover{transform:translateY(-1px)}@media(max-width:700px){.chat-header-inner{width:100%;min-height:58px;padding:0 14px}.brand-sub{display:none}.support-nav a,.text-button{min-height:44px;font-size:14px}.app[data-view=landing] .thread-inner{width:100%;padding:44px 20px calc(132px + env(safe-area-inset-bottom))}.thread-inner,.composer-inner{width:100%;padding-left:16px;padding-right:16px}.landing-hero h1{font-size:2.25rem;line-height:1.05}.landing-lede{margin-top:16px;font-size:14px}.ask-composer{position:fixed;z-index:10;right:76px;bottom:max(16px,env(safe-area-inset-bottom));left:16px;width:auto;min-height:54px;margin:0;border-radius:27px}.landing-trust{font-size:11.5px}.topics-section{margin-top:52px}.topic-grid{grid-template-columns:1fr;gap:31px}.topics-heading>a,.topic-card h3 a,.topic-card li a{min-height:44px;display:flex;align-items:center}.topic-card ul{margin-top:4px;gap:0}.human-help-button{position:fixed;z-index:10;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:48px;min-height:48px;justify-content:center;margin:0;padding:0;border-radius:50%;font-size:16px}.human-help-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.thread-inner{padding-top:24px}.thread-list{padding-bottom:28px}.bubble-stack{max-width:100%}.bubble-row>.avatar{display:none}.composer-inner{padding-top:8px;padding-bottom:max(16px,env(safe-area-inset-bottom))}.composer input{font-size:16px}.composer .mic-button:not(.mic-button--active){display:none}.send-button{min-width:54px;padding:0 12px}.follow-up-chip{min-height:44px;font-size:16px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.typing-dots{display:none}.typing-static{display:inline}} +:root{color-scheme:light;font-family:var(--font, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif);color:var(--ink, #121212);background:var(--canvas, #ffffff);font-synthesis:none;--card: #ffffff;--soft: color-mix(in srgb, var(--ink, #121212) 4%, #ffffff);--line: color-mix(in srgb, var(--ink, #121212) 12%, transparent);--line-strong: color-mix(in srgb, var(--ink, #121212) 22%, transparent);--muted: color-mix(in srgb, var(--ink, #121212) 62%, var(--canvas, #ffffff));--accent-soft: color-mix(in srgb, var(--accent, #c87942) 9%, #ffffff);--accent-line: color-mix(in srgb, var(--accent, #c87942) 34%, transparent);--focus-ring: color-mix(in srgb, var(--accent, #c87942) 50%, transparent);--surface-width: 64rem;--answer-width: 47.5rem}*{box-sizing:border-box}html,body{min-height:100%}html.support-conversation,body.support-conversation{height:100%;overflow:hidden;overscroll-behavior:none}body{margin:0;min-width:320px;font-family:inherit;background:var(--canvas, #fff)}button,input{font:inherit}button{border:0;cursor:pointer;touch-action:manipulation}button:disabled{cursor:not-allowed;opacity:.45}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.skip-link{position:fixed;z-index:20;top:8px;left:8px;transform:translateY(-150%);border-radius:8px;padding:10px 14px;color:var(--ink, #121212);background:#fff;box-shadow:0 2px 12px #0000001f;font-weight:700}.skip-link:focus{transform:translateY(0)}.app{min-height:100dvh;display:flex;flex-direction:column}.app[data-view=conversation]{position:fixed;z-index:1;top:var(--support-viewport-top, 0px);right:0;left:0;width:100%;min-height:0;height:var(--support-viewport-height, 100dvh);max-height:var(--support-viewport-height, 100dvh);overflow:hidden}.chat-header{flex:none;padding-top:env(safe-area-inset-top);background:var(--card)}.chat-header-inner{width:min(var(--surface-width),100% - 40px);min-height:64px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.header-actions,.support-nav{display:flex;align-items:center;gap:8px}.support-nav a,.text-button{min-height:44px;display:inline-flex;align-items:center;border-radius:9px;padding:0 9px;color:var(--muted);background:transparent;font-size:13px;font-weight:650;text-decoration:none;white-space:nowrap}.support-nav a:hover,.text-button:not(:disabled):hover{color:var(--ink, #121212);background:var(--soft)}.brand{min-width:0;display:inline-flex;align-items:center;gap:10px;color:inherit;text-decoration:none}.brand-logo{display:block;height:28px;max-width:150px;object-fit:contain}.brand-mark,.avatar{flex:none;display:grid;place-items:center;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-size:11px;font-weight:800}.brand-mark{width:32px;height:32px;border-radius:9px}.avatar{width:28px;height:28px;border-radius:50%}.brand-text{min-width:0;display:grid;line-height:1.25}.brand-name{overflow:hidden;font-size:15px;font-weight:720;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.brand-sub{color:var(--muted);font-size:12px}.reconnect-banner{flex:none;padding:8px 16px;border-bottom:1px solid var(--accent-line);color:var(--ink, #121212);background:var(--accent-soft);text-align:center;font-size:13px;font-weight:650}.thread{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-padding-block:24px;scrollbar-color:var(--line-strong) transparent}.thread-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:36px 0 24px}.app[data-view=landing] .thread{flex:none;overflow:visible}.app[data-view=landing] .thread-inner{width:min(var(--surface-width),100% - 40px);padding:104px 0 80px}.landing-hero h1{max-width:18ch;margin:0;font-size:clamp(2.5rem,4vw,3rem);font-weight:650;letter-spacing:-.045em;line-height:1}.landing-lede{max-width:42rem;margin:20px 0 0;color:var(--muted);font-size:15px;line-height:1.55}.ask-composer{width:100%;min-height:52px;margin-top:30px;display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:13px;padding:4px 5px 4px 10px;background:#fff;box-shadow:0 1px #0000000a,0 6px 18px #0000000f}.ask-composer:focus-within{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--focus-ring),0 8px 24px #00000014}.ask-icon{flex:none;display:grid;place-items:center}.ask-icon .avatar{width:24px;height:24px;font-size:9px}.ask-composer input{flex:1 1 auto;min-width:0;min-height:44px;border:0;padding:0 3px;color:var(--ink, #121212);background:transparent;font-size:16px;outline:0}.ask-composer input::placeholder,.composer input::placeholder{color:var(--muted)}.ask-composer input:focus-visible{outline:0}.ask-composer .mic-button,.ask-composer .ask-send-button{width:44px;height:44px;min-width:44px;min-height:44px;padding:0}.ask-composer .mic-button{border:0}.ask-send-button{border-radius:9px;display:grid;place-items:center}.landing-trust{margin:13px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.landing-status{min-height:18px;margin:6px 0 0;color:var(--ink, #121212);font-size:12px;font-weight:650;line-height:1.5}.topics-section{margin-top:72px}.topics-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.topics-heading h2{margin:0;font-size:21px;font-weight:670;letter-spacing:-.025em}.topics-heading>a{color:var(--muted);font-size:13px;font-weight:650}.topic-grid{margin-top:27px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));column-gap:56px;row-gap:42px}.topic-card h3{margin:0;font-size:14px;font-weight:720;line-height:1.35}.topic-card h3 a,.topic-card li a{color:var(--ink, #121212);text-decoration:none}.topic-card h3 a:hover,.topic-card li a:hover{text-decoration:underline;text-underline-offset:3px}.topic-card>p{margin:7px 0 0;color:var(--muted);font-size:12.5px;line-height:1.5}.topic-card ul{margin:12px 0 0;padding:0;list-style:none;display:grid;gap:8px}.topic-card li{font-size:13px;line-height:1.45}.topics-empty{grid-column:1 / -1;margin:0;color:var(--muted)}.human-help-button{position:fixed;z-index:5;right:18px;bottom:max(16px,env(safe-area-inset-bottom));min-height:48px;display:inline-flex;align-items:center;gap:9px;border:1px solid var(--line-strong);border-radius:12px;padding:0 15px 0 10px;color:var(--ink, #121212);background:#fff;box-shadow:0 4px 16px #00000017;font-size:13px;font-weight:700}.human-help-button:hover{transform:translateY(-1px);box-shadow:0 6px 20px #0000001f}.human-help-button .avatar{width:26px;height:26px;font-size:9px}.human-help-icon{display:grid;place-items:center}.thread-list{margin:0;padding:0 0 36px;list-style:none;display:flex;flex-direction:column;gap:28px}.answer-turn{display:grid;gap:8px}.answer-turn--user{width:100%}.turn-label{margin:0;color:var(--muted);font-size:12px;font-weight:700}.turn-copy{color:var(--ink, #121212);font-size:15px;line-height:1.62}.turn-copy p{margin:0;overflow-wrap:anywhere}.answer-turn--user .turn-copy{font-size:clamp(1.35rem,4vw,1.5rem);font-weight:650;letter-spacing:-.025em;line-height:1.25}.answer-turn--user .turn-copy h2{margin:0;overflow-wrap:anywhere;font:inherit}.answer-turn--pending{opacity:.65}.typing-bubble{min-height:25px}.typing-dots{display:inline-flex;gap:4px;align-items:center}.typing-dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing-blink 1.2s ease-in-out infinite}.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}@keyframes typing-blink{0%,80%,to{opacity:.3}40%{opacity:1}}.typing-static{display:none;font-weight:700;letter-spacing:2px}.system-note{padding:2px 8px;color:var(--muted);text-align:center;font-size:12.5px;line-height:1.45}.sources-row{margin-top:-18px}.sources-card{width:100%}.sources-card summary{width:max-content;min-height:44px;display:flex;align-items:center;border-radius:7px;padding:5px 7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer}.sources-card[open] summary{margin-bottom:8px}.sources-list{display:flex;flex-wrap:wrap;gap:7px}.source-chip{min-height:44px;max-width:100%;display:inline-flex;flex-direction:column;justify-content:center;gap:1px;border-radius:8px;padding:7px 9px;color:inherit;background:var(--soft);text-decoration:none}.source-chip:hover{background:var(--accent-soft)}.source-chip span{color:var(--ink, #121212);font-size:12.5px;font-weight:650}.source-chip small{color:var(--muted);font-size:10.5px}.follow-up-prompts{margin-top:9px}.follow-up-prompts>p{margin:0 0 7px;color:var(--muted);font-size:12px;font-weight:650}.follow-up-list{display:flex;flex-wrap:wrap;gap:7px}.follow-up-chip{min-height:36px;border-radius:8px;padding:7px 10px;color:var(--ink, #121212);background:var(--soft);font-size:12px;font-weight:600;text-align:left}.follow-up-chip:hover{background:var(--accent-soft)}.bubble-row{display:flex;align-items:flex-start;gap:10px}.bubble-stack{width:100%;max-width:31rem;display:grid;gap:10px}.chat-card{display:grid;gap:11px;border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--card)}.chat-card-lead{margin:0;font-size:14px;font-weight:680}.chat-card-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.contact-field,.code-field{display:grid;gap:6px}.contact-field span,.code-field span{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.contact-field input,.code-field input{min-width:0;min-height:44px;border:1px solid var(--line-strong);border-radius:9px;padding:0 12px;color:var(--ink, #121212);background:#fff;font-size:16px}.code-field input{min-height:48px;font-size:20px;font-weight:750;letter-spacing:.24em;text-align:center}.contact-field input::placeholder{color:var(--muted)}.access-button{min-height:44px;border-radius:9px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.access-button:not(:disabled):hover{transform:translateY(-1px)}.access-feedback{color:var(--muted);font-size:11.5px;line-height:1.45}.session-turnstile,.verify-turnstile{overflow:hidden}.session-turnstile{position:fixed;z-index:30;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:min(20rem,calc(100% - 32px));pointer-events:none}.session-turnstile:not(:empty){min-height:65px;pointer-events:auto}.session-turnstile:empty,.verify-turnstile:empty{display:none}.cf-turnstile{min-height:65px;overflow:hidden}.verify-form{display:grid;gap:11px}.eyebrow{margin:0;color:var(--accent-text, var(--accent, #c87942));font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.ticket-card{gap:6px}.ticket-title{margin:0;font-size:15px;font-weight:700}.ticket-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.ticket-card dl{margin:4px 0 0;display:flex;gap:22px}.ticket-card dl div{display:grid;gap:3px}.ticket-card dt{color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.ticket-card dd{margin:0;font-size:13px;font-weight:700}.composer-bar{flex:none;border-top:1px solid var(--line);background:color-mix(in srgb,var(--card) 94%,transparent);backdrop-filter:blur(12px)}.composer-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:10px 0 max(8px,env(safe-area-inset-bottom))}.composer{display:flex;align-items:center;gap:8px}.composer input{flex:1 1 auto;min-width:0;min-height:46px;border:1px solid var(--line-strong);border-radius:12px;padding:0 14px;color:var(--ink, #121212);background:#fff;font-size:16px}.mic-button,.human-action-button{width:46px;height:46px;flex:none;display:grid;place-items:center;border:1px solid var(--line-strong);border-radius:12px;color:var(--ink, #121212);background:var(--card)}.mic-button--active{border-color:transparent;color:var(--accent-ink, #000);background:var(--accent, #c87942);animation:mic-pulse 1.6s ease-in-out infinite}@keyframes mic-pulse{50%{box-shadow:0 0 0 8px color-mix(in srgb,var(--accent, #c87942) 18%,transparent)}}.mute-button{min-height:44px;flex:none;border:1px solid var(--line-strong);border-radius:10px;padding:0 13px;color:var(--ink, #121212);background:var(--card);font-size:12.5px;font-weight:650}.send-button{min-height:46px;flex:none;border-radius:10px;padding:0 18px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.send-button:not(:disabled):hover,.mic-button:not(:disabled):hover,.human-action-button:not(:disabled):hover{transform:translateY(-1px)}@media(max-width:700px){.chat-header-inner{width:100%;min-height:58px;padding:0 14px}.brand-sub{display:none}.support-nav a,.text-button{min-height:44px;font-size:14px}.app[data-view=landing] .thread-inner{width:100%;padding:44px 20px calc(132px + env(safe-area-inset-bottom))}.thread-inner,.composer-inner{width:100%;padding-left:16px;padding-right:16px}.landing-hero h1{font-size:2.25rem;line-height:1.05}.landing-lede{margin-top:16px;font-size:14px}.ask-composer{position:fixed;z-index:10;right:76px;bottom:max(16px,env(safe-area-inset-bottom));left:16px;width:auto;min-height:54px;margin:0;border-radius:27px}.landing-trust{font-size:11.5px}.topics-section{margin-top:52px}.topic-grid{grid-template-columns:1fr;gap:31px}.topics-heading>a,.topic-card h3 a,.topic-card li a{min-height:44px;display:flex;align-items:center}.topic-card ul{margin-top:4px;gap:0}.human-help-button{position:fixed;z-index:10;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:48px;min-height:48px;justify-content:center;margin:0;padding:0;border-radius:50%;font-size:16px}.human-help-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.thread-inner{padding-top:24px}.thread-list{padding-bottom:28px}.bubble-stack{max-width:100%}.bubble-row>.avatar{display:none}.composer-inner{padding-top:8px;padding-bottom:max(16px,env(safe-area-inset-bottom))}.composer input{font-size:16px}.composer .mic-button:not(.mic-button--active){display:none}.send-button{min-width:54px;padding:0 12px}.follow-up-chip{min-height:44px;font-size:16px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.typing-dots{display:none}.typing-static{display:inline}} diff --git a/apps/desk/scripts/visual-server.mjs b/apps/desk/scripts/visual-server.mjs index e7a9f0a..602c81e 100644 --- a/apps/desk/scripts/visual-server.mjs +++ b/apps/desk/scripts/visual-server.mjs @@ -33,6 +33,12 @@ await run(['d1', 'migrations', 'apply', 'DB', '--local', '--persist-to', persist await run(['d1', 'execute', 'DB', '--local', '--persist-to', persistence, '--file', seed, '--config', config]) const developmentEmail = process.env.MORROW_DEV_EMAIL ?? 'owner@example.com' +const optionalBindings = [ + 'SHOPIFY_SHOP_DOMAIN', + 'SHOPIFY_ADMIN_TOKEN', + 'SHOPIFY_CLIENT_ID', + 'SHOPIFY_CLIENT_SECRET', +].flatMap((name) => process.env[name] ? ['--var', `${name}:${process.env[name]}`] : []) const server = spawn(wrangler, [ 'dev', '--ip', '127.0.0.1', @@ -40,6 +46,7 @@ const server = spawn(wrangler, [ '--persist-to', persistence, '--var', `MORROW_DEV_EMAIL:${developmentEmail}`, '--var', 'MORROW_VOICE_DEMO_ENABLED:1', + ...optionalBindings, '--config', config, ], { cwd: root, diff --git a/apps/desk/src/voice/demo-agent.ts b/apps/desk/src/voice/demo-agent.ts index 46db1be..3fa7165 100644 --- a/apps/desk/src/voice/demo-agent.ts +++ b/apps/desk/src/voice/demo-agent.ts @@ -17,6 +17,8 @@ import { voiceDemoEnabled } from './demo-page' import { hasVoiceContact, normalizeVoiceContact, + ORDER_LOOKUP_CONTACT_CONTINUATION, + PRODUCT_HELP_CONTACT_CONTINUATION, type VoiceContact, } from './contact' import { @@ -36,7 +38,12 @@ import { voiceAgentSystemPrompt, } from './conversation' import { shopifyConfigured } from '../integrations/shopify' -import { orderStatusForSession } from './orders' +import { + findOrderNumber, + isOrderLookupRequest, + orderStatusForSession, + orderStatusReply, +} from './orders' import { createVoiceVerification, verifyVoiceVerificationCode, @@ -238,10 +245,28 @@ export class MorrowDeskAgent extends VoiceAgent { } } + const ordersAvailable = shopifyConfigured(this.env) + if (!contact && ordersAvailable && isOrderLookupRequest(transcript)) { + context.connection.setState({ + ...state, + pendingContactReason: 'order_lookup', + } satisfies VoiceConnectionState) + context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply' })) + return 'To look up your order, add your name and the email used at checkout in the card below.' + } + + const isOrderContinuation = transcript.trim() === ORDER_LOOKUP_CONTACT_CONTINUATION + || transcript.trim() === PRODUCT_HELP_CONTACT_CONTINUATION + if (contact && ordersAvailable && isOrderContinuation) { + const orderNumber = findOrderNumber(messages.filter((message) => message.content.trim() !== transcript.trim())) + if (!orderNumber) return 'What is the order number from your confirmation email?' + const result = await orderStatusForSession(this.env, { email: contact.email }, orderNumber) + return orderStatusReply(result) + } + const directResponse = directVoiceResponse(transcript, messages) if (directResponse) return directResponse - const ordersAvailable = shopifyConfigured(this.env) const ordersEnabled = ordersAvailable && contact !== null let contactCardRequested = false const settings = await loadWorkspaceSettings(this.env.DB) diff --git a/apps/desk/src/voice/demo.css b/apps/desk/src/voice/demo.css index da66a48..2439f44 100644 --- a/apps/desk/src/voice/demo.css +++ b/apps/desk/src/voice/demo.css @@ -157,7 +157,7 @@ button:focus-visible, input:focus-visible, a:focus-visible, summary:focus-visibl .ask-composer input { flex: 1 1 auto; min-width: 0; min-height: 44px; border: 0; padding: 0 3px; color: var(--ink, #121212); background: transparent; font-size: 16px; outline: 0; } .ask-composer input::placeholder, .composer input::placeholder { color: var(--muted); } .ask-composer input:focus-visible { outline: 0; } -.ask-composer .mic-button, .ask-send-button { width: 44px; height: 44px; min-height: 44px; padding: 0; } +.ask-composer .mic-button, .ask-composer .ask-send-button { width: 44px; height: 44px; min-width: 44px; min-height: 44px; padding: 0; } .ask-composer .mic-button { border: 0; } .ask-send-button { border-radius: 9px; display: grid; place-items: center; } .landing-trust { margin: 13px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; } diff --git a/apps/desk/src/voice/orders.ts b/apps/desk/src/voice/orders.ts index c9a54e8..51b0573 100644 --- a/apps/desk/src/voice/orders.ts +++ b/apps/desk/src/voice/orders.ts @@ -14,6 +14,47 @@ export type OrderStatusToolResult = | { status: 'not_found' } | { status: 'unavailable' } +type ConversationMessage = { role: 'user' | 'assistant'; content: string } + +const ORDER_LOOKUP_REQUEST = /(?:\b(?:where|track|tracking|status|shipped|delivery|arrive|arrival)\b.{0,60}\border\b|\border\b.{0,60}\b(?:where|track|tracking|status|shipped|delivery|arrive|arrival)\b)/i +const ORDER_NUMBER = /(?:\border(?:\s+(?:number|no\.?))?\s*[:#-]?\s*((?=[A-Z0-9-]*\d)[A-Z0-9][A-Z0-9-]{1,31})\b|#((?=[A-Z0-9-]*\d)[A-Z0-9][A-Z0-9-]{1,31})\b)/i + +export function isOrderLookupRequest(message: string): boolean { + return ORDER_LOOKUP_REQUEST.test(message) +} + +export function findOrderNumber(messages: ConversationMessage[]): string | null { + for (let index = messages.length - 1; index >= 0; index--) { + const message = messages[index] + if (message?.role !== 'user') continue + const match = ORDER_NUMBER.exec(message.content) + const value = match?.[1] ?? match?.[2] + if (value) return `#${value.replace(/^#/, '').toUpperCase()}` + } + return null +} + +export function orderStatusReply(result: OrderStatusToolResult): string { + if (result.status === 'not_found') { + return 'I could not find that order for the email on this session. Double-check the order number, or start over with the email used at checkout. I can also open a support ticket for the team.' + } + if (result.status === 'unavailable') { + return 'I’m having trouble checking orders right now. I can open a support ticket so the team can follow up.' + } + + const { order } = result + const statuses = [ + order.financialStatus ? `payment is ${order.financialStatus.toLowerCase()}` : null, + order.fulfillmentStatus ? `fulfillment is ${order.fulfillmentStatus.toLowerCase()}` : null, + ].filter((value): value is string => value !== null) + const tracking = order.tracking + .map((entry) => entry.number) + .filter((value): value is string => value !== null) + const statusCopy = statuses.length > 0 ? ` ${statuses.join(' and ')}.` : '' + const trackingCopy = tracking.length > 0 ? ` Tracking number: ${tracking.join(', ')}.` : '' + return `I found ${order.name}.${statusCopy}${trackingCopy}` +} + /** * Order read-back authorization is possession of the (order number, contact * email) pair — the same standard as Shopify's own order-status page. The diff --git a/apps/desk/test/visual/portal.visual.spec.ts b/apps/desk/test/visual/portal.visual.spec.ts index 9c9543c..25ca159 100644 --- a/apps/desk/test/visual/portal.visual.spec.ts +++ b/apps/desk/test/visual/portal.visual.spec.ts @@ -270,11 +270,22 @@ test('support home opens with the agent as the primary help experience', async ( const box = element.getBoundingClientRect() return { top: box.top, bottom: box.bottom, viewportHeight: window.innerHeight } }) + const sendGeometry = await page.getByRole('button', { name: 'Send question' }).evaluate((element) => { + const button = element.getBoundingClientRect() + const icon = element.querySelector('svg')?.getBoundingClientRect() + return { + width: button.width, + height: button.height, + iconOffsetX: icon ? (icon.left + icon.width / 2) - (button.left + button.width / 2) : null, + iconOffsetY: icon ? (icon.top + icon.height / 2) - (button.top + button.height / 2) : null, + } + }) const width = viewportWidth(testInfo) if (width <= 700) { expect(composerPosition.top).toBeGreaterThan(composerPosition.viewportHeight * .65) expect(composerPosition.bottom).toBeLessThanOrEqual(composerPosition.viewportHeight - 12) + expect(sendGeometry).toEqual({ width: 44, height: 44, iconOffsetX: 0, iconOffsetY: 0 }) } else { expect(composerPosition.top).toBeLessThan(composerPosition.viewportHeight * .55) } diff --git a/apps/desk/test/voice-agent.test.ts b/apps/desk/test/voice-agent.test.ts index ccf051d..ea2d11e 100644 --- a/apps/desk/test/voice-agent.test.ts +++ b/apps/desk/test/voice-agent.test.ts @@ -2,6 +2,7 @@ import { env } from 'cloudflare:test' import { describe, expect, it } from 'vitest' import { HUMAN_HELP_MESSAGE } from '../src/voice/escalation' +import { ORDER_LOOKUP_CONTACT_CONTINUATION } from '../src/voice/contact' type TestAgentEnv = typeof env & { MorrowDeskAgent: DurableObjectNamespace } @@ -151,6 +152,37 @@ describe('voice agent WebSocket boundary', () => { } }) + it('continues an order request after contact without repeating the identity request', async () => { + const socket = await connectAgent(`order-contact-${crypto.randomUUID()}`) + try { + await proveSession(socket) + const cardRequested = nextMessage(socket, (message) => message.type === 'voice_contact_required') + const initialReply = nextMessage(socket, (message) => message.type === 'transcript_end') + socket.send(JSON.stringify({ type: 'text_message', text: 'Where is my order?' })) + + await expect(cardRequested).resolves.toMatchObject({ anchor: 'after_reply' }) + await expect(initialReply).resolves.toMatchObject({ + text: 'To look up your order, add your name and the email used at checkout in the card below.', + }) + + const contactSet = nextMessage(socket, (message) => message.type === 'voice_contact_set') + socket.send(JSON.stringify({ + type: 'set_voice_contact', + name: 'Local Order Tester', + email: 'local-order@example.test', + })) + await expect(contactSet).resolves.toMatchObject({ continuation: 'order_lookup' }) + + const lookupReply = nextMessage(socket, (message) => message.type === 'transcript_end') + socket.send(JSON.stringify({ type: 'text_message', text: ORDER_LOOKUP_CONTACT_CONTINUATION })) + const result = await lookupReply + expect(result).toMatchObject({ text: 'What is the order number from your confirmation email?' }) + expect(String(result.text)).not.toMatch(/add your name|email.*card/i) + } finally { + socket.close() + } + }) + it('stores unverified contact details once and creates a real voice ticket', async () => { await env.DB.prepare(`UPDATE workspace_settings SET support_email = 'support@example.test', diff --git a/apps/desk/test/voice-orders.test.ts b/apps/desk/test/voice-orders.test.ts index 2ccac87..6537fd5 100644 --- a/apps/desk/test/voice-orders.test.ts +++ b/apps/desk/test/voice-orders.test.ts @@ -5,7 +5,12 @@ import { SHOPIFY_API_VERSION, shopifyConfigured, } from '../src/integrations/shopify' -import { orderStatusForSession } from '../src/voice/orders' +import { + findOrderNumber, + isOrderLookupRequest, + orderStatusForSession, + orderStatusReply, +} from '../src/voice/orders' const ENV = { SHOPIFY_SHOP_DOMAIN: 'example-store.myshopify.com', SHOPIFY_ADMIN_TOKEN: 'shpat-test-token' } const CC_ENV = { @@ -149,6 +154,33 @@ describe('shopify adapter', () => { }) }) +describe('deterministic voice order continuation', () => { + it('recognizes an order-status request and preserves a previously supplied order number', () => { + expect(isOrderLookupRequest('Where is my order?')).toBe(true) + expect(isOrderLookupRequest('Can you track order #FC-4021 for me?')).toBe(true) + expect(isOrderLookupRequest('How do I clean my grinder?')).toBe(false) + expect(findOrderNumber([ + { role: 'user', content: 'Where is order #FC-4021?' }, + { role: 'assistant', content: 'Add your email in the card below.' }, + { role: 'user', content: 'I have shared my name and email.' }, + ])).toBe('#FC-4021') + }) + + it('renders provider results without asking for contact details again', () => { + expect(orderStatusReply({ status: 'not_found' })).toContain('email on this session') + expect(orderStatusReply({ status: 'unavailable' })).toContain('trouble checking orders') + expect(orderStatusReply({ status: 'ok', order: ORDER_SUMMARY })).toContain('TRACK123') + + for (const result of [ + { status: 'not_found' } as const, + { status: 'unavailable' } as const, + { status: 'ok', order: ORDER_SUMMARY } as const, + ]) { + expect(orderStatusReply(result)).not.toMatch(/name|email.*card|contact details/i) + } + }) +}) + describe('shopify client credentials grant', () => { beforeEach(() => resetShopifyTokenCache()) diff --git a/apps/desk/vitest.config.ts b/apps/desk/vitest.config.ts index b1a92d9..b73f95d 100644 --- a/apps/desk/vitest.config.ts +++ b/apps/desk/vitest.config.ts @@ -13,6 +13,8 @@ export default defineConfig({ bindings: { MORROW_DEV_EMAIL: 'owner@example.com', MORROW_OPERATOR_HOSTNAME: 'operators.example.test', + SHOPIFY_SHOP_DOMAIN: 'shop.example.test', + SHOPIFY_ADMIN_TOKEN: 'local-test-token', VOICE_TEST_OTP_CODE: '123456', WHATSAPP_VERIFY_TOKEN: 'whatsapp-test-verify-token', WHATSAPP_APP_SECRET: 'whatsapp-test-app-secret', From 1600e8d13ff91a47f2ac8a20ef643e5d4a35a9f2 Mon Sep 17 00:00:00 2001 From: Morrow Contributors Date: Thu, 6 Aug 2026 20:38:08 +0800 Subject: [PATCH 3/6] feat: make support entry task-led MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from AbleDesk PR #49 (6e06146), mechanism only. The landing page now offers a "Common tasks" grid of scoped starting points backed by data-support-message, moves "Contact support" into that grid, and links private-request recovery. Contact-card labels become addressable so the card can explain which reason it is collecting for. HUMAN_HELP_MESSAGE now names the real outcome — a private email-support request, not a live transfer — and the explicit_human_request rule matches the new wording. Fix Coffee's product copy is deliberately NOT ported: Morrow keeps its neutral headings, "Ask anything" placeholder, branded lede, and generic test fixtures. The India-specific "GST invoice" tile is generalised to "Billing or invoice". Order-number fixtures use a neutral prefix. public/voice-demo.js and public/voice-demo.css are build outputs and are rebuilt separately. --- apps/desk/public/voice-demo.css | 2 +- apps/desk/src/voice/demo-agent.ts | 6 +- apps/desk/src/voice/demo-client.ts | 114 +++++++++++++++++--- apps/desk/src/voice/demo-page.ts | 55 ++++++++-- apps/desk/src/voice/demo.css | 77 ++++++------- apps/desk/src/voice/escalation.ts | 6 +- apps/desk/test/visual/portal.visual.spec.ts | 48 +++++++-- apps/desk/test/voice-agent.test.ts | 6 +- apps/desk/test/voice-conversation.test.ts | 2 +- apps/desk/test/voice-demo.test.ts | 36 +++++-- apps/desk/test/voice-orders.test.ts | 8 +- docs/voice-demo.md | 6 +- 12 files changed, 259 insertions(+), 107 deletions(-) diff --git a/apps/desk/public/voice-demo.css b/apps/desk/public/voice-demo.css index 2b6ee06..3d07a2d 100644 --- a/apps/desk/public/voice-demo.css +++ b/apps/desk/public/voice-demo.css @@ -1 +1 @@ -:root{color-scheme:light;font-family:var(--font, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif);color:var(--ink, #121212);background:var(--canvas, #ffffff);font-synthesis:none;--card: #ffffff;--soft: color-mix(in srgb, var(--ink, #121212) 4%, #ffffff);--line: color-mix(in srgb, var(--ink, #121212) 12%, transparent);--line-strong: color-mix(in srgb, var(--ink, #121212) 22%, transparent);--muted: color-mix(in srgb, var(--ink, #121212) 62%, var(--canvas, #ffffff));--accent-soft: color-mix(in srgb, var(--accent, #c87942) 9%, #ffffff);--accent-line: color-mix(in srgb, var(--accent, #c87942) 34%, transparent);--focus-ring: color-mix(in srgb, var(--accent, #c87942) 50%, transparent);--surface-width: 64rem;--answer-width: 47.5rem}*{box-sizing:border-box}html,body{min-height:100%}html.support-conversation,body.support-conversation{height:100%;overflow:hidden;overscroll-behavior:none}body{margin:0;min-width:320px;font-family:inherit;background:var(--canvas, #fff)}button,input{font:inherit}button{border:0;cursor:pointer;touch-action:manipulation}button:disabled{cursor:not-allowed;opacity:.45}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.skip-link{position:fixed;z-index:20;top:8px;left:8px;transform:translateY(-150%);border-radius:8px;padding:10px 14px;color:var(--ink, #121212);background:#fff;box-shadow:0 2px 12px #0000001f;font-weight:700}.skip-link:focus{transform:translateY(0)}.app{min-height:100dvh;display:flex;flex-direction:column}.app[data-view=conversation]{position:fixed;z-index:1;top:var(--support-viewport-top, 0px);right:0;left:0;width:100%;min-height:0;height:var(--support-viewport-height, 100dvh);max-height:var(--support-viewport-height, 100dvh);overflow:hidden}.chat-header{flex:none;padding-top:env(safe-area-inset-top);background:var(--card)}.chat-header-inner{width:min(var(--surface-width),100% - 40px);min-height:64px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.header-actions,.support-nav{display:flex;align-items:center;gap:8px}.support-nav a,.text-button{min-height:44px;display:inline-flex;align-items:center;border-radius:9px;padding:0 9px;color:var(--muted);background:transparent;font-size:13px;font-weight:650;text-decoration:none;white-space:nowrap}.support-nav a:hover,.text-button:not(:disabled):hover{color:var(--ink, #121212);background:var(--soft)}.brand{min-width:0;display:inline-flex;align-items:center;gap:10px;color:inherit;text-decoration:none}.brand-logo{display:block;height:28px;max-width:150px;object-fit:contain}.brand-mark,.avatar{flex:none;display:grid;place-items:center;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-size:11px;font-weight:800}.brand-mark{width:32px;height:32px;border-radius:9px}.avatar{width:28px;height:28px;border-radius:50%}.brand-text{min-width:0;display:grid;line-height:1.25}.brand-name{overflow:hidden;font-size:15px;font-weight:720;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.brand-sub{color:var(--muted);font-size:12px}.reconnect-banner{flex:none;padding:8px 16px;border-bottom:1px solid var(--accent-line);color:var(--ink, #121212);background:var(--accent-soft);text-align:center;font-size:13px;font-weight:650}.thread{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-padding-block:24px;scrollbar-color:var(--line-strong) transparent}.thread-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:36px 0 24px}.app[data-view=landing] .thread{flex:none;overflow:visible}.app[data-view=landing] .thread-inner{width:min(var(--surface-width),100% - 40px);padding:104px 0 80px}.landing-hero h1{max-width:18ch;margin:0;font-size:clamp(2.5rem,4vw,3rem);font-weight:650;letter-spacing:-.045em;line-height:1}.landing-lede{max-width:42rem;margin:20px 0 0;color:var(--muted);font-size:15px;line-height:1.55}.ask-composer{width:100%;min-height:52px;margin-top:30px;display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:13px;padding:4px 5px 4px 10px;background:#fff;box-shadow:0 1px #0000000a,0 6px 18px #0000000f}.ask-composer:focus-within{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--focus-ring),0 8px 24px #00000014}.ask-icon{flex:none;display:grid;place-items:center}.ask-icon .avatar{width:24px;height:24px;font-size:9px}.ask-composer input{flex:1 1 auto;min-width:0;min-height:44px;border:0;padding:0 3px;color:var(--ink, #121212);background:transparent;font-size:16px;outline:0}.ask-composer input::placeholder,.composer input::placeholder{color:var(--muted)}.ask-composer input:focus-visible{outline:0}.ask-composer .mic-button,.ask-composer .ask-send-button{width:44px;height:44px;min-width:44px;min-height:44px;padding:0}.ask-composer .mic-button{border:0}.ask-send-button{border-radius:9px;display:grid;place-items:center}.landing-trust{margin:13px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.landing-status{min-height:18px;margin:6px 0 0;color:var(--ink, #121212);font-size:12px;font-weight:650;line-height:1.5}.topics-section{margin-top:72px}.topics-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.topics-heading h2{margin:0;font-size:21px;font-weight:670;letter-spacing:-.025em}.topics-heading>a{color:var(--muted);font-size:13px;font-weight:650}.topic-grid{margin-top:27px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));column-gap:56px;row-gap:42px}.topic-card h3{margin:0;font-size:14px;font-weight:720;line-height:1.35}.topic-card h3 a,.topic-card li a{color:var(--ink, #121212);text-decoration:none}.topic-card h3 a:hover,.topic-card li a:hover{text-decoration:underline;text-underline-offset:3px}.topic-card>p{margin:7px 0 0;color:var(--muted);font-size:12.5px;line-height:1.5}.topic-card ul{margin:12px 0 0;padding:0;list-style:none;display:grid;gap:8px}.topic-card li{font-size:13px;line-height:1.45}.topics-empty{grid-column:1 / -1;margin:0;color:var(--muted)}.human-help-button{position:fixed;z-index:5;right:18px;bottom:max(16px,env(safe-area-inset-bottom));min-height:48px;display:inline-flex;align-items:center;gap:9px;border:1px solid var(--line-strong);border-radius:12px;padding:0 15px 0 10px;color:var(--ink, #121212);background:#fff;box-shadow:0 4px 16px #00000017;font-size:13px;font-weight:700}.human-help-button:hover{transform:translateY(-1px);box-shadow:0 6px 20px #0000001f}.human-help-button .avatar{width:26px;height:26px;font-size:9px}.human-help-icon{display:grid;place-items:center}.thread-list{margin:0;padding:0 0 36px;list-style:none;display:flex;flex-direction:column;gap:28px}.answer-turn{display:grid;gap:8px}.answer-turn--user{width:100%}.turn-label{margin:0;color:var(--muted);font-size:12px;font-weight:700}.turn-copy{color:var(--ink, #121212);font-size:15px;line-height:1.62}.turn-copy p{margin:0;overflow-wrap:anywhere}.answer-turn--user .turn-copy{font-size:clamp(1.35rem,4vw,1.5rem);font-weight:650;letter-spacing:-.025em;line-height:1.25}.answer-turn--user .turn-copy h2{margin:0;overflow-wrap:anywhere;font:inherit}.answer-turn--pending{opacity:.65}.typing-bubble{min-height:25px}.typing-dots{display:inline-flex;gap:4px;align-items:center}.typing-dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing-blink 1.2s ease-in-out infinite}.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}@keyframes typing-blink{0%,80%,to{opacity:.3}40%{opacity:1}}.typing-static{display:none;font-weight:700;letter-spacing:2px}.system-note{padding:2px 8px;color:var(--muted);text-align:center;font-size:12.5px;line-height:1.45}.sources-row{margin-top:-18px}.sources-card{width:100%}.sources-card summary{width:max-content;min-height:44px;display:flex;align-items:center;border-radius:7px;padding:5px 7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer}.sources-card[open] summary{margin-bottom:8px}.sources-list{display:flex;flex-wrap:wrap;gap:7px}.source-chip{min-height:44px;max-width:100%;display:inline-flex;flex-direction:column;justify-content:center;gap:1px;border-radius:8px;padding:7px 9px;color:inherit;background:var(--soft);text-decoration:none}.source-chip:hover{background:var(--accent-soft)}.source-chip span{color:var(--ink, #121212);font-size:12.5px;font-weight:650}.source-chip small{color:var(--muted);font-size:10.5px}.follow-up-prompts{margin-top:9px}.follow-up-prompts>p{margin:0 0 7px;color:var(--muted);font-size:12px;font-weight:650}.follow-up-list{display:flex;flex-wrap:wrap;gap:7px}.follow-up-chip{min-height:36px;border-radius:8px;padding:7px 10px;color:var(--ink, #121212);background:var(--soft);font-size:12px;font-weight:600;text-align:left}.follow-up-chip:hover{background:var(--accent-soft)}.bubble-row{display:flex;align-items:flex-start;gap:10px}.bubble-stack{width:100%;max-width:31rem;display:grid;gap:10px}.chat-card{display:grid;gap:11px;border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--card)}.chat-card-lead{margin:0;font-size:14px;font-weight:680}.chat-card-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.contact-field,.code-field{display:grid;gap:6px}.contact-field span,.code-field span{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.contact-field input,.code-field input{min-width:0;min-height:44px;border:1px solid var(--line-strong);border-radius:9px;padding:0 12px;color:var(--ink, #121212);background:#fff;font-size:16px}.code-field input{min-height:48px;font-size:20px;font-weight:750;letter-spacing:.24em;text-align:center}.contact-field input::placeholder{color:var(--muted)}.access-button{min-height:44px;border-radius:9px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.access-button:not(:disabled):hover{transform:translateY(-1px)}.access-feedback{color:var(--muted);font-size:11.5px;line-height:1.45}.session-turnstile,.verify-turnstile{overflow:hidden}.session-turnstile{position:fixed;z-index:30;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:min(20rem,calc(100% - 32px));pointer-events:none}.session-turnstile:not(:empty){min-height:65px;pointer-events:auto}.session-turnstile:empty,.verify-turnstile:empty{display:none}.cf-turnstile{min-height:65px;overflow:hidden}.verify-form{display:grid;gap:11px}.eyebrow{margin:0;color:var(--accent-text, var(--accent, #c87942));font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.ticket-card{gap:6px}.ticket-title{margin:0;font-size:15px;font-weight:700}.ticket-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.ticket-card dl{margin:4px 0 0;display:flex;gap:22px}.ticket-card dl div{display:grid;gap:3px}.ticket-card dt{color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.ticket-card dd{margin:0;font-size:13px;font-weight:700}.composer-bar{flex:none;border-top:1px solid var(--line);background:color-mix(in srgb,var(--card) 94%,transparent);backdrop-filter:blur(12px)}.composer-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:10px 0 max(8px,env(safe-area-inset-bottom))}.composer{display:flex;align-items:center;gap:8px}.composer input{flex:1 1 auto;min-width:0;min-height:46px;border:1px solid var(--line-strong);border-radius:12px;padding:0 14px;color:var(--ink, #121212);background:#fff;font-size:16px}.mic-button,.human-action-button{width:46px;height:46px;flex:none;display:grid;place-items:center;border:1px solid var(--line-strong);border-radius:12px;color:var(--ink, #121212);background:var(--card)}.mic-button--active{border-color:transparent;color:var(--accent-ink, #000);background:var(--accent, #c87942);animation:mic-pulse 1.6s ease-in-out infinite}@keyframes mic-pulse{50%{box-shadow:0 0 0 8px color-mix(in srgb,var(--accent, #c87942) 18%,transparent)}}.mute-button{min-height:44px;flex:none;border:1px solid var(--line-strong);border-radius:10px;padding:0 13px;color:var(--ink, #121212);background:var(--card);font-size:12.5px;font-weight:650}.send-button{min-height:46px;flex:none;border-radius:10px;padding:0 18px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.send-button:not(:disabled):hover,.mic-button:not(:disabled):hover,.human-action-button:not(:disabled):hover{transform:translateY(-1px)}@media(max-width:700px){.chat-header-inner{width:100%;min-height:58px;padding:0 14px}.brand-sub{display:none}.support-nav a,.text-button{min-height:44px;font-size:14px}.app[data-view=landing] .thread-inner{width:100%;padding:44px 20px calc(132px + env(safe-area-inset-bottom))}.thread-inner,.composer-inner{width:100%;padding-left:16px;padding-right:16px}.landing-hero h1{font-size:2.25rem;line-height:1.05}.landing-lede{margin-top:16px;font-size:14px}.ask-composer{position:fixed;z-index:10;right:76px;bottom:max(16px,env(safe-area-inset-bottom));left:16px;width:auto;min-height:54px;margin:0;border-radius:27px}.landing-trust{font-size:11.5px}.topics-section{margin-top:52px}.topic-grid{grid-template-columns:1fr;gap:31px}.topics-heading>a,.topic-card h3 a,.topic-card li a{min-height:44px;display:flex;align-items:center}.topic-card ul{margin-top:4px;gap:0}.human-help-button{position:fixed;z-index:10;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:48px;min-height:48px;justify-content:center;margin:0;padding:0;border-radius:50%;font-size:16px}.human-help-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.thread-inner{padding-top:24px}.thread-list{padding-bottom:28px}.bubble-stack{max-width:100%}.bubble-row>.avatar{display:none}.composer-inner{padding-top:8px;padding-bottom:max(16px,env(safe-area-inset-bottom))}.composer input{font-size:16px}.composer .mic-button:not(.mic-button--active){display:none}.send-button{min-width:54px;padding:0 12px}.follow-up-chip{min-height:44px;font-size:16px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.typing-dots{display:none}.typing-static{display:inline}} +:root{color-scheme:light;font-family:var(--font, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif);color:var(--ink, #121212);background:var(--canvas, #ffffff);font-synthesis:none;--card: #ffffff;--soft: color-mix(in srgb, var(--ink, #121212) 4%, #ffffff);--line: color-mix(in srgb, var(--ink, #121212) 12%, transparent);--line-strong: color-mix(in srgb, var(--ink, #121212) 22%, transparent);--muted: color-mix(in srgb, var(--ink, #121212) 62%, var(--canvas, #ffffff));--accent-soft: color-mix(in srgb, var(--accent, #c87942) 9%, #ffffff);--accent-line: color-mix(in srgb, var(--accent, #c87942) 34%, transparent);--focus-ring: color-mix(in srgb, var(--accent, #c87942) 50%, transparent);--surface-width: 64rem;--answer-width: 47.5rem}*{box-sizing:border-box}html,body{min-height:100%}html.support-conversation,body.support-conversation{height:100%;overflow:hidden;overscroll-behavior:none}body{margin:0;min-width:320px;font-family:inherit;background:var(--canvas, #fff)}button,input{font:inherit}button{border:0;cursor:pointer;touch-action:manipulation}button:disabled{cursor:not-allowed;opacity:.45}button:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.skip-link{position:fixed;z-index:20;top:8px;left:8px;transform:translateY(-150%);border-radius:8px;padding:10px 14px;color:var(--ink, #121212);background:#fff;box-shadow:0 2px 12px #0000001f;font-weight:700}.skip-link:focus{transform:translateY(0)}.app{min-height:100dvh;display:flex;flex-direction:column}.app[data-view=conversation]{position:fixed;z-index:1;top:var(--support-viewport-top, 0px);right:0;left:0;width:100%;min-height:0;height:var(--support-viewport-height, 100dvh);max-height:var(--support-viewport-height, 100dvh);overflow:hidden}.chat-header{flex:none;padding-top:env(safe-area-inset-top);background:var(--card)}.chat-header-inner{width:min(var(--surface-width),100% - 40px);min-height:64px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.header-actions,.support-nav{display:flex;align-items:center;gap:8px}.support-nav a,.text-button{min-height:44px;display:inline-flex;align-items:center;border-radius:9px;padding:0 9px;color:var(--muted);background:transparent;font-size:13px;font-weight:650;text-decoration:none;white-space:nowrap}.support-nav a:hover,.text-button:not(:disabled):hover{color:var(--ink, #121212);background:var(--soft)}.brand{min-width:0;display:inline-flex;align-items:center;gap:10px;color:inherit;text-decoration:none}.brand-logo{display:block;height:28px;max-width:150px;object-fit:contain}.brand-mark,.avatar{flex:none;display:grid;place-items:center;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-size:11px;font-weight:800}.brand-mark{width:32px;height:32px;border-radius:9px}.avatar{width:28px;height:28px;border-radius:50%}.brand-text{min-width:0;display:grid;line-height:1.25}.brand-name{overflow:hidden;font-size:15px;font-weight:720;letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap}.brand-sub{color:var(--muted);font-size:12px}.reconnect-banner{flex:none;padding:8px 16px;border-bottom:1px solid var(--accent-line);color:var(--ink, #121212);background:var(--accent-soft);text-align:center;font-size:13px;font-weight:650}.thread{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-padding-block:24px;scrollbar-color:var(--line-strong) transparent}.thread-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:36px 0 24px}.app[data-view=landing] .thread{flex:none;overflow:visible}.app[data-view=landing] .thread-inner{width:min(var(--surface-width),100% - 40px);padding:104px 0 80px}.landing-hero h1{max-width:18ch;margin:0;font-size:clamp(2.5rem,4vw,3rem);font-weight:650;letter-spacing:-.045em;line-height:1}.landing-lede{max-width:42rem;margin:20px 0 0;color:var(--muted);font-size:15px;line-height:1.55}.ask-composer{width:100%;min-height:52px;margin-top:30px;display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:13px;padding:4px 5px 4px 10px;background:#fff;box-shadow:0 1px #0000000a,0 6px 18px #0000000f}.ask-composer:focus-within{border-color:var(--line-strong);box-shadow:0 0 0 3px var(--focus-ring),0 8px 24px #00000014}.ask-icon{flex:none;display:grid;place-items:center}.ask-icon .avatar{width:24px;height:24px;font-size:9px}.ask-composer input{flex:1 1 auto;min-width:0;min-height:44px;border:0;padding:0 3px;color:var(--ink, #121212);background:transparent;font-size:16px;outline:0}.ask-composer input::placeholder,.composer input::placeholder{color:var(--muted)}.ask-composer input:focus-visible{outline:0}.ask-composer .mic-button,.ask-composer .ask-send-button{width:44px;height:44px;min-width:44px;min-height:44px;padding:0}.ask-composer .mic-button{border:0}.ask-send-button{border-radius:9px;display:grid;place-items:center}.landing-trust{margin:13px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.landing-status{min-height:18px;margin:6px 0 0;color:var(--ink, #121212);font-size:12px;font-weight:650;line-height:1.5}.support-tasks{margin-top:48px}.support-tasks-heading .eyebrow{margin:0}.support-tasks-heading h2{margin:5px 0 0;font-size:21px;font-weight:670;letter-spacing:-.025em}.support-task-grid{margin-top:20px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.support-task{min-height:124px;display:flex;flex-direction:column;justify-content:space-between;gap:16px;border:1px solid var(--line);border-radius:12px;padding:16px;color:var(--ink, #121212);background:var(--card);text-align:left;text-decoration:none;transition:border-color .15s ease,box-shadow .15s ease,transform .15s ease}.support-task:not(:disabled):hover{border-color:var(--line-strong);box-shadow:0 6px 18px #0000000f;transform:translateY(-1px)}.support-task--contact{border-color:var(--accent-line);background:var(--accent-soft)}.support-task-title{font-size:14px;font-weight:720;line-height:1.35}.support-task-copy{color:var(--muted);font-size:12px;line-height:1.45}.topics-section{margin-top:72px}.topics-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.topics-heading h2{margin:0;font-size:21px;font-weight:670;letter-spacing:-.025em}.topics-heading>a{color:var(--muted);font-size:13px;font-weight:650}.topic-grid{margin-top:27px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));column-gap:56px;row-gap:42px}.topic-card h3{margin:0;font-size:14px;font-weight:720;line-height:1.35}.topic-card h3 a,.topic-card li a{color:var(--ink, #121212);text-decoration:none}.topic-card h3 a:hover,.topic-card li a:hover{text-decoration:underline;text-underline-offset:3px}.topic-card>p{margin:7px 0 0;color:var(--muted);font-size:12.5px;line-height:1.5}.topic-card ul{margin:12px 0 0;padding:0;list-style:none;display:grid;gap:8px}.topic-card li{font-size:13px;line-height:1.45}.topics-empty{grid-column:1 / -1;margin:0;color:var(--muted)}.thread-list{margin:0;padding:0 0 36px;list-style:none;display:flex;flex-direction:column;gap:28px}.answer-turn{display:grid;gap:8px}.answer-turn--user{width:100%}.turn-label{margin:0;color:var(--muted);font-size:12px;font-weight:700}.turn-copy{color:var(--ink, #121212);font-size:15px;line-height:1.62}.turn-copy p{margin:0;overflow-wrap:anywhere}.answer-turn--user .turn-copy{font-size:clamp(1.35rem,4vw,1.5rem);font-weight:650;letter-spacing:-.025em;line-height:1.25}.answer-turn--user .turn-copy h2{margin:0;overflow-wrap:anywhere;font:inherit}.answer-turn--pending{opacity:.65}.typing-bubble{min-height:25px}.typing-dots{display:inline-flex;gap:4px;align-items:center}.typing-dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:typing-blink 1.2s ease-in-out infinite}.typing-dots span:nth-child(2){animation-delay:.2s}.typing-dots span:nth-child(3){animation-delay:.4s}@keyframes typing-blink{0%,80%,to{opacity:.3}40%{opacity:1}}.typing-static{display:none;font-weight:700;letter-spacing:2px}.system-note{padding:2px 8px;color:var(--muted);text-align:center;font-size:12.5px;line-height:1.45}.sources-row{margin-top:-18px}.sources-card{width:100%}.sources-card summary{width:max-content;min-height:44px;display:flex;align-items:center;border-radius:7px;padding:5px 7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer}.sources-card[open] summary{margin-bottom:8px}.sources-list{display:flex;flex-wrap:wrap;gap:7px}.source-chip{min-height:44px;max-width:100%;display:inline-flex;flex-direction:column;justify-content:center;gap:1px;border-radius:8px;padding:7px 9px;color:inherit;background:var(--soft);text-decoration:none}.source-chip:hover{background:var(--accent-soft)}.source-chip span{color:var(--ink, #121212);font-size:12.5px;font-weight:650}.source-chip small{color:var(--muted);font-size:10.5px}.follow-up-prompts{margin-top:9px}.follow-up-prompts>p{margin:0 0 7px;color:var(--muted);font-size:12px;font-weight:650}.follow-up-list{display:flex;flex-wrap:wrap;gap:7px}.follow-up-chip{min-height:36px;border-radius:8px;padding:7px 10px;color:var(--ink, #121212);background:var(--soft);font-size:12px;font-weight:600;text-align:left}.follow-up-chip:hover{background:var(--accent-soft)}.bubble-row{display:flex;align-items:flex-start;gap:10px}.bubble-stack{width:100%;max-width:31rem;display:grid;gap:10px}.chat-card{display:grid;gap:11px;border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--card)}.chat-card-lead{margin:0;font-size:14px;font-weight:680}.chat-card-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.contact-field,.code-field{display:grid;gap:6px}.contact-field span,.code-field span{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.contact-field input,.code-field input{min-width:0;min-height:44px;border:1px solid var(--line-strong);border-radius:9px;padding:0 12px;color:var(--ink, #121212);background:#fff;font-size:16px}.code-field input{min-height:48px;font-size:20px;font-weight:750;letter-spacing:.24em;text-align:center}.contact-field input::placeholder{color:var(--muted)}.access-button{min-height:44px;border-radius:9px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.access-button:not(:disabled):hover{transform:translateY(-1px)}.access-feedback{color:var(--muted);font-size:11.5px;line-height:1.45}.session-turnstile,.verify-turnstile{overflow:hidden}.session-turnstile{position:fixed;z-index:30;right:16px;bottom:max(16px,env(safe-area-inset-bottom));width:min(20rem,calc(100% - 32px));pointer-events:none}.session-turnstile:not(:empty){min-height:65px;pointer-events:auto}.session-turnstile:empty,.verify-turnstile:empty{display:none}.cf-turnstile{min-height:65px;overflow:hidden}.verify-form{display:grid;gap:11px}.eyebrow{margin:0;color:var(--accent-text, var(--accent, #c87942));font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.ticket-card{gap:6px}.ticket-title{margin:0;font-size:15px;font-weight:700}.ticket-copy{margin:0;color:var(--muted);font-size:12.5px;line-height:1.5}.ticket-card dl{margin:4px 0 0;display:flex;gap:22px}.ticket-card dl div{display:grid;gap:3px}.ticket-card dt{color:var(--muted);font-size:10px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.ticket-card dd{margin:0;font-size:13px;font-weight:700}.composer-bar{flex:none;border-top:1px solid var(--line);background:color-mix(in srgb,var(--card) 94%,transparent);backdrop-filter:blur(12px)}.composer-inner{width:min(var(--answer-width),100% - 40px);margin:0 auto;padding:10px 0 max(8px,env(safe-area-inset-bottom))}.composer{display:flex;align-items:center;gap:8px}.composer input{flex:1 1 auto;min-width:0;min-height:46px;border:1px solid var(--line-strong);border-radius:12px;padding:0 14px;color:var(--ink, #121212);background:#fff;font-size:16px}.mic-button,.human-action-button{width:46px;height:46px;flex:none;display:grid;place-items:center;border:1px solid var(--line-strong);border-radius:12px;color:var(--ink, #121212);background:var(--card)}.mic-button--active{border-color:transparent;color:var(--accent-ink, #000);background:var(--accent, #c87942);animation:mic-pulse 1.6s ease-in-out infinite}@keyframes mic-pulse{50%{box-shadow:0 0 0 8px color-mix(in srgb,var(--accent, #c87942) 18%,transparent)}}.mute-button{min-height:44px;flex:none;border:1px solid var(--line-strong);border-radius:10px;padding:0 13px;color:var(--ink, #121212);background:var(--card);font-size:12.5px;font-weight:650}.send-button{min-height:46px;flex:none;border-radius:10px;padding:0 18px;color:var(--accent-ink, #000);background:var(--accent, #c87942);font-weight:700}.send-button:not(:disabled):hover,.mic-button:not(:disabled):hover,.human-action-button:not(:disabled):hover{transform:translateY(-1px)}@media(max-width:700px){.chat-header-inner{width:100%;min-height:58px;padding:0 14px}.brand-sub{display:none}.support-nav a,.text-button{min-height:44px;font-size:14px}.app[data-view=landing] .thread-inner{width:100%;padding:44px 20px 80px}.thread-inner,.composer-inner{width:100%;padding-left:16px;padding-right:16px}.landing-hero h1{font-size:2.25rem;line-height:1.05}.landing-lede{margin-top:16px;font-size:14px}.ask-composer{position:static;width:100%;min-height:54px;margin-top:20px;border-radius:13px}.landing-trust{font-size:11.5px}.support-tasks{margin-top:38px}.support-task-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.support-task{min-height:132px;padding:14px}.topics-section{margin-top:52px}.topic-grid{grid-template-columns:1fr;gap:31px}.topics-heading>a,.topic-card h3 a,.topic-card li a{min-height:44px;display:flex;align-items:center}.topic-card ul{margin-top:4px;gap:0}.thread-inner{padding-top:24px}.thread-list{padding-bottom:28px}.bubble-stack{max-width:100%}.bubble-row>.avatar{display:none}.composer-inner{padding-top:8px;padding-bottom:max(16px,env(safe-area-inset-bottom))}.composer input{font-size:16px}.composer .mic-button:not(.mic-button--active){display:none}.send-button{min-width:54px;padding:0 12px}.follow-up-chip{min-height:44px;font-size:16px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.typing-dots{display:none}.typing-static{display:inline}} diff --git a/apps/desk/src/voice/demo-agent.ts b/apps/desk/src/voice/demo-agent.ts index 3fa7165..0702f8d 100644 --- a/apps/desk/src/voice/demo-agent.ts +++ b/apps/desk/src/voice/demo-agent.ts @@ -228,7 +228,7 @@ export class MorrowDeskAgent extends VoiceAgent { requestId: turnRequestId, }, } satisfies VoiceConnectionState) - context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply' })) + context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply', reason: 'open_ticket' })) return 'This needs a person from the team to review it. Add your email in the card below and I will open a ticket for you right away.' } try { @@ -251,7 +251,7 @@ export class MorrowDeskAgent extends VoiceAgent { ...state, pendingContactReason: 'order_lookup', } satisfies VoiceConnectionState) - context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply' })) + context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply', reason: 'order_lookup' })) return 'To look up your order, add your name and the email used at checkout in the card below.' } @@ -367,7 +367,7 @@ export class MorrowDeskAgent extends VoiceAgent { ...connectionState(context.connection), pendingContactReason: continuationReason, } satisfies VoiceConnectionState) - context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply' })) + context.connection.send(JSON.stringify({ type: 'voice_contact_required', anchor: 'after_reply', reason: continuationReason })) return reason === 'product_help' ? { status: 'requested', diff --git a/apps/desk/src/voice/demo-client.ts b/apps/desk/src/voice/demo-client.ts index 695cd9c..a71bfa0 100644 --- a/apps/desk/src/voice/demo-client.ts +++ b/apps/desk/src/voice/demo-client.ts @@ -24,6 +24,9 @@ const elements = { composerBar: required('#composer-bar'), contactFlow: required('#contact-flow'), contactForm: required('#contact-form'), + contactCardLead: required('#contact-card-lead'), + contactNameLabel: required('#contact-name-label'), + contactEmailLabel: required('#contact-email-label'), contactName: required('#contact-name'), contactEmail: required('#contact-email'), contactSubmit: required('#contact-submit'), @@ -51,6 +54,8 @@ const elements = { verifyFeedback: required('#verify-feedback'), } +const supportTaskButtons = [...document.querySelectorAll('[data-support-message]')] + const RESET_FOCUS_KEY = 'morrow-voice-support-reset-focus' function sessionName(): string { @@ -72,6 +77,9 @@ let sessionReady = false let contactReady = false let contactPending = false +type ContactReason = 'order_lookup' | 'open_ticket' | 'product_help' +let contactReason: ContactReason | null = null + type SourceArticle = { title: string; section: string; url: string } let latestMessages: TranscriptMessage[] = [] @@ -224,7 +232,7 @@ function sourcesRow(anchor: number, articles: SourceArticle[], open: boolean): H card.dataset.anchor = String(anchor) card.open = open const summary = document.createElement('summary') - summary.textContent = `Show sources (${articles.length})` + summary.textContent = `Based on (${articles.length})` card.append(summary) const list = document.createElement('div') list.className = 'sources-list' @@ -294,10 +302,10 @@ function renderThread(): void { } const stick = isNearBottom() const length = latestMessages.length - const openSourceAnchors = new Set( - [...elements.transcript.querySelectorAll('.sources-card[open]')] - .map((details) => Number(details.dataset.anchor)) - .filter(Number.isFinite), + const sourceOpenState = new Map( + [...elements.transcript.querySelectorAll('.sources-card')] + .map((details) => [Number(details.dataset.anchor), details.open] as const) + .filter(([anchor]) => Number.isFinite(anchor)), ) const rows: Node[] = [] const pushExtras = (index: number): void => { @@ -306,7 +314,7 @@ function renderThread(): void { } for (const entry of sources) { if (entry.anchor <= length && entry.anchor === index) { - rows.push(sourcesRow(entry.anchor, entry.articles, openSourceAnchors.has(entry.anchor))) + rows.push(sourcesRow(entry.anchor, entry.articles, sourceOpenState.get(entry.anchor) ?? true)) } } if (contactAnchor !== null && contactAnchor <= length && contactAnchor === index) { @@ -377,6 +385,7 @@ function updateControls(status: VoiceStatus): void { elements.app.setAttribute('aria-busy', String(!ready)) elements.landingInput.disabled = false elements.landingSubmit.disabled = !ready + for (const task of supportTaskButtons) task.disabled = !ready elements.humanHelpButton.disabled = !ready elements.textInput.disabled = false elements.textSubmit.disabled = !ready @@ -390,11 +399,56 @@ function updateControls(status: VoiceStatus): void { } } +function applyContactCardCopy(): void { + const copy = contactReason === 'order_lookup' + ? { + lead: 'Find your order', + name: 'Name', + email: 'Email used at checkout', + feedback: 'We use your email with the order number to protect your details. Your name is used if support needs to follow up.', + submit: 'Continue to order lookup', + } + : contactReason === 'open_ticket' + ? { + lead: 'Open a private support request', + name: 'Name', + email: 'Email for your private case link', + feedback: 'We use this address for this private support request.', + submit: 'Open support request', + } + : contactReason === 'product_help' + ? { + lead: 'Send this to support', + name: 'Name', + email: 'Email for follow-up', + feedback: 'We’ll use these details only for this support request.', + submit: 'Continue', + } + : { + lead: 'Share support details', + name: 'Name', + email: 'Email', + feedback: 'Used only for the support action you requested.', + submit: 'Continue', + } + elements.contactCardLead.textContent = copy.lead + elements.contactNameLabel.textContent = copy.name + elements.contactEmailLabel.textContent = copy.email + elements.contactFeedback.textContent = copy.feedback + elements.contactSubmit.dataset.readyLabel = copy.submit +} + function updateContactCard(): void { elements.contactSubmit.disabled = !connected || contactPending elements.contactName.disabled = contactPending elements.contactEmail.disabled = contactPending - elements.contactSubmit.textContent = contactPending ? 'Saving…' : 'Continue' + elements.contactSubmit.textContent = contactPending ? 'Saving…' : elements.contactSubmit.dataset.readyLabel ?? 'Continue' +} + +function readContactReason(value: unknown): ContactReason | null { + return value === 'order_lookup' || value === 'open_ticket' || value === 'product_help' + ? value + : null } function showContactCard(anchor: 'now' | 'after_reply'): void { @@ -606,14 +660,19 @@ function renderCustomMessage(value: unknown): void { return } if (message.type === 'voice_contact_set') { + const completedReason = contactReason contactReady = true contactPending = false hideContactCard() - updateContactCard() updateControls(client.status) const contact = message.contact as { email?: unknown } | undefined const email = typeof contact?.email === 'string' ? contact.email : 'your email' - pushNote(`We’ll use ${email} for this support action`) + pushNote(completedReason === 'order_lookup' + ? `We’ll use ${email} only to look up this order` + : `We’ll use ${email} for this support action`) + contactReason = null + applyContactCardCopy() + updateContactCard() // This is a machine-readable continuation, not customer copy. It lets Ava // resume the interrupted flow while keeping the transcript natural. const continuation = contactContinuationMessage(message.continuation) @@ -634,6 +693,8 @@ function renderCustomMessage(value: unknown): void { if (message.type === 'voice_contact_required') { contactReady = false contactPending = false + contactReason = readContactReason(message.reason) + applyContactCardCopy() showContactCard(message.anchor === 'after_reply' ? 'after_reply' : 'now') return } @@ -648,6 +709,9 @@ function renderCustomMessage(value: unknown): void { hideVerifyCard() elements.contactName.value = '' elements.contactEmail.value = '' + contactReason = null + applyContactCardCopy() + updateContactCard() updateControls(client.status) if (resetReload !== null) { clearTimeout(resetReload) @@ -835,6 +899,8 @@ elements.clearButton.addEventListener('click', () => { elements.textInput.value = '' elements.contactName.value = '' elements.contactEmail.value = '' + contactReason = null + applyContactCardCopy() setConversationMode(false, true) renderThread() updateControls(client.status) @@ -847,12 +913,29 @@ function sendMessage(message: string): boolean { return true } +function focusSupportRequest(): void { + if (!isReady()) return + elements.landingInput.placeholder = 'Briefly describe what you need help with' + elements.landingStatus.textContent = 'Describe the issue first. Ava will ask for email only if a private request is needed.' + elements.landingInput.focus() +} + +function requestHumanHelpForConversation(): void { + const latestCustomerMessage = [...latestMessages].reverse().find((message) => message.role === 'user')?.text.trim() + if (!latestCustomerMessage) { + elements.textInput.placeholder = 'Briefly describe what you need help with' + elements.textInput.focus() + return + } + sendMessage(`${HUMAN_HELP_MESSAGE} My issue is: ${latestCustomerMessage}`) +} + elements.landingForm.addEventListener('submit', (event) => { event.preventDefault() const message = elements.landingInput.value.trim() if (!message) return if (!isReady()) { - elements.landingStatus.textContent = 'Loading help…' + elements.landingStatus.textContent = 'Preparing secure chat…' return } elements.landingInput.blur() @@ -860,11 +943,18 @@ elements.landingForm.addEventListener('submit', (event) => { }) elements.humanHelpButton.addEventListener('click', () => { - sendMessage(HUMAN_HELP_MESSAGE) + focusSupportRequest() }) +for (const task of supportTaskButtons) { + task.addEventListener('click', () => { + const message = task.dataset.supportMessage + if (message) sendMessage(message) + }) +} + elements.conversationHumanButton.addEventListener('click', () => { - sendMessage(HUMAN_HELP_MESSAGE) + requestHumanHelpForConversation() }) elements.textForm.addEventListener('submit', (event) => { diff --git a/apps/desk/src/voice/demo-page.ts b/apps/desk/src/voice/demo-page.ts index 8cdc02c..6b5b005 100644 --- a/apps/desk/src/voice/demo-page.ts +++ b/apps/desk/src/voice/demo-page.ts @@ -83,6 +83,13 @@ export function voiceDemoPageResponse( ? `\n ` : '' const avatar = `` + const orderTaskCopy = ordersEnabled + ? 'Use your checkout email and order number.' + : 'Ask Ava for the available tracking steps.' + const orderTaskMarkup = `` const topicMarkup = topics.length > 0 ? topics.slice(0, 9).map((topic) => `

${escapeText(topic.name)}

@@ -133,7 +140,7 @@ export function voiceDemoPageResponse(

How can we help?

-

Ask Ava anything about ${escapeText(workspaceShortName(branding.displayName))}. She answers from our help articles and can bring in the team when you need them.

+

Ask Ava anything about ${escapeText(workspaceShortName(branding.displayName))}, start a common task, or open a private support request. She answers from our help articles and can bring in the team when you need them.

@@ -144,8 +151,38 @@ export function voiceDemoPageResponse(
-

Ask without identifying yourself. Ava requests contact details only for ${ordersEnabled ? 'order help or ' : ''}team follow-up.

-

+

Start anonymously. Ava asks only for what she needs for ${ordersEnabled ? 'an order lookup or ' : ''}email support.

+

Preparing secure chat…

+
+ +
+
+

Common tasks

+

Choose a starting point

+
+
+ ${orderTaskMarkup} + + + + + + Find a request + Recover a private case link securely. + +
@@ -158,10 +195,6 @@ export function voiceDemoPageResponse(
-