fix ui view

This commit is contained in:
javad committed 2026-09-17 09:41:15 +03:30
1 parent 863ca89485
commit 83f4d7c728
9 files changed
+182 -17

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
+8 -3
View File
@@ -11,6 +11,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
const atBottom=()=>scroller.scrollHeight-scroller.scrollTop-scroller.clientHeight<80;
const scrollLatest=(smooth=false)=>{const top=scroller.scrollHeight;if(smooth&&typeof scroller.scrollTo==='function')scroller.scrollTo({top,behavior:'smooth'});else scroller.scrollTop=top};
let followLatest=true;
let initialPin=true;
const scheduleLatest=()=>{[0,40,120,300,700,1200].forEach(delay=>setTimeout(()=>{if(initialPin)scrollLatest(false)},delay))};
scheduleLatest();
@@ -22,8 +23,12 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
sizeObserver?.observe(scroller);if(conversationEnd?.parentElement)sizeObserver?.observe(conversationEnd.parentElement);
setTimeout(()=>{initialPin=false;sizeObserver?.disconnect()},1800);
['wheel','touchstart','pointerdown','keydown'].forEach(eventName=>scroller.addEventListener(eventName,()=>{initialPin=false;sizeObserver?.disconnect()},{once:true,passive:true}));
scroller.addEventListener('scroll',()=>{jump.hidden=atBottom()});
scroller.addEventListener('scroll',()=>{followLatest=atBottom();jump.hidden=followLatest});
jump?.addEventListener('click',()=>{scrollLatest(true);jump.hidden=true});
const layoutObserver='ResizeObserver' in window?new ResizeObserver(()=>{if(followLatest)requestAnimationFrame(()=>scrollLatest(false))}):null;
layoutObserver?.observe(scroller);
if(composer)layoutObserver?.observe(composer);
window.addEventListener('appviewportchange',()=>{if(followLatest)requestAnimationFrame(()=>scrollLatest(false))});
const groupDates=()=>{scroller.querySelectorAll('.date-divider').forEach(item=>item.remove());let previousDate='';scroller.querySelectorAll('[data-message]').forEach(message=>{if(message.dataset.date!==previousDate){const divider=document.createElement('div');divider.className='date-divider';divider.innerHTML=`<span>${message.dataset.dayLabel}</span>`;message.before(divider);previousDate=message.dataset.date}})};
groupDates();
@@ -36,7 +41,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
const replyResult=workspace.dataset.replyResult||new URLSearchParams(location.search).get('reply_result')||'';
const submissionSucceeded=replyResult==='reply'||replyResult==='note';
if(submissionSucceeded){sessionStorage.removeItem(`${baseKey}-reply`);sessionStorage.removeItem(`${baseKey}-note`)}
const setComposerOpen=(open,focus=true)=>{const keepLatestVisible=atBottom();composer?.classList.toggle('is-collapsed',!open);composerToggle?.setAttribute('aria-expanded',String(open));requestAnimationFrame(()=>{if(keepLatestVisible)scrollLatest(false)});if(open&&focus)setTimeout(()=>input?.focus(),80)};
const setComposerOpen=(open,focus=true)=>{const keepLatestVisible=followLatest||atBottom();composer?.classList.toggle('is-collapsed',!open);composerToggle?.setAttribute('aria-expanded',String(open));[0,80,240].forEach(delay=>setTimeout(()=>{if(keepLatestVisible)scrollLatest(false)},delay));if(open&&focus)setTimeout(()=>input?.focus({preventScroll:true}),80)};
const saveDraft=()=>{if(!input)return;sessionStorage.setItem(draftKey(),input.value);if(status){status.textContent=input.value?'پیش‌نویس ذخیره شد':'';clearTimeout(saveDraft.timer);saveDraft.timer=setTimeout(()=>status.textContent='',1800)}};
const loadDraft=()=>{if(!input)return;input.value=sessionStorage.getItem(draftKey())||'';resize();input.dispatchEvent(new Event('input',{bubbles:true}))};
if(input){input.setAttribute('rows','5');input.setAttribute('placeholder','شرح کامل پاسخ، اطلاعات تکمیلی و نتیجه مورد انتظار خود را بنویسید…');input.addEventListener('input',()=>{resize();saveDraft()});loadDraft()}
@@ -117,7 +122,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
// cancel a recursively scheduled timeout. pollRunning prevents overlap and
// nextPollAt preserves the normal interval and network-error backoff.
const pollHeartbeat=setInterval(startPolling,1000);
window.addEventListener('beforeunload',()=>{clearInterval(pollHeartbeat);stopPolling()},{once:true});
window.addEventListener('beforeunload',()=>{clearInterval(pollHeartbeat);stopPolling();layoutObserver?.disconnect()},{once:true});
startPolling();
const resetMessageMenu=menu=>{if(!menu)return;const editor=menu.querySelector('[data-message-editor]');const choices=menu.querySelector('[data-message-menu-choices]');const input=menu.querySelector('[data-message-edit-input]');if(editor)editor.hidden=true;if(choices)choices.hidden=false;if(input){input.value=input.dataset.originalValue||'';input.dispatchEvent(new Event('input',{bubbles:true}))}};
+1 -1
View File
@@ -6,7 +6,7 @@ const isiOS=/iphone|ipad|ipod/i.test(navigator.userAgent);
const dismissed=()=>Date.now()-Number(localStorage.getItem('pwa-install-dismissed')||0)<24*60*60*1000;
const show=()=>{if(banner&&mobileDevice&&!standalone&&!dismissed())banner.hidden=false};
const setGuide=(text,label='راهنمای نصب')=>{if(help)help.textContent=text;if(accept)accept.textContent=label;banner?.classList.add('pwa-install-guidance')};
if('serviceWorker' in navigator&&window.isSecureContext){let workerRegistration=null,workerReloading=false;const assetVersion=document.documentElement.dataset.staticVersion||new URL(document.currentScript?.src||location.href).searchParams.get('v')||'';const serviceWorkerUrl=`/service-worker.js?v=${encodeURIComponent(assetVersion)}`;const updateWorker=()=>{if(workerRegistration&&document.visibilityState==='visible')workerRegistration.update().catch(()=>{})};navigator.serviceWorker.addEventListener('controllerchange',()=>{if(workerReloading)return;workerReloading=true;location.reload()});window.addEventListener('load',async()=>{try{workerRegistration=await navigator.serviceWorker.register(serviceWorkerUrl,{updateViaCache:'none'});await workerRegistration.update()}catch(error){setGuide('فعال‌سازی نصب برنامه با خطا روبه‌رو شد. صفحه را تازه‌سازی کنید.');show()}});document.addEventListener('visibilitychange',updateWorker);setInterval(updateWorker,60000)}
if('serviceWorker' in navigator&&window.isSecureContext){let workerRegistration=null,workerReloading=false,hasWorkerController=Boolean(navigator.serviceWorker.controller);const assetVersion=document.documentElement.dataset.staticVersion||new URL(document.currentScript?.src||location.href).searchParams.get('v')||'';const serviceWorkerUrl=`/service-worker.js?v=${encodeURIComponent(assetVersion)}`;const updateWorker=()=>{if(workerRegistration&&document.visibilityState==='visible')workerRegistration.update().catch(()=>{})};navigator.serviceWorker.addEventListener('controllerchange',()=>{if(!hasWorkerController){hasWorkerController=true;return}if(workerReloading)return;workerReloading=true;location.reload()});window.addEventListener('load',async()=>{try{workerRegistration=await navigator.serviceWorker.register(serviceWorkerUrl,{updateViaCache:'none'});await workerRegistration.update()}catch(error){setGuide('فعال‌سازی نصب برنامه با خطا روبه‌رو شد. صفحه را تازه‌سازی کنید.');show()}});document.addEventListener('visibilitychange',updateWorker);setInterval(updateWorker,60000)}
window.addEventListener('beforeinstallprompt',event=>{event.preventDefault();deferredInstallPrompt=event;if(accept){accept.textContent='نصب برنامه';accept.disabled=false}banner?.classList.remove('pwa-install-guidance');if(help)help.textContent='برنامه آماده نصب است؛ برای ادامه روی «نصب برنامه» بزنید.';show()});
if(mobileDevice&&isiOS&&!standalone){setGuide('در Safari روی دکمه اشتراک‌گذاری (□↑) بزنید و «Add to Home Screen» را انتخاب کنید.','نمایش راهنما');setTimeout(show,700)}
if(mobileDevice&&!isiOS&&!standalone)setTimeout(()=>{if(!deferredInstallPrompt){setGuide(window.isSecureContext?'از منوی مرورگر گزینه «Install app» یا «افزودن به صفحه اصلی» را انتخاب کنید.':'نصب مستقیم روی آدرس HTTP شبکه ممکن نیست. سامانه را با HTTPS باز کنید؛ سپس دکمه نصب فعال می‌شود.','راهنمای نصب');show()}},1800);
+24
View File
@@ -1,3 +1,27 @@
// Keep a live measurement of the visual viewport for mobile browsers whose
// address bar or software keyboard does not resize the layout viewport. CSS
// combines this value with 100svh, so address-bar animation cannot make the
// ticket shell jump while a genuinely smaller keyboard viewport still fits.
(()=>{
let frame=0;
const syncViewport=()=>{
frame=0;
const viewport=window.visualViewport;
const height=Math.max(1,Math.round(viewport?.height||window.innerHeight));
const offsetTop=Math.max(0,Math.round(viewport?.offsetTop||0));
const root=document.documentElement;
root.style.setProperty('--visual-viewport-height',`${height}px`);
root.style.setProperty('--visual-viewport-offset-top',`${offsetTop}px`);
window.dispatchEvent(new CustomEvent('appviewportchange',{detail:{height,offsetTop}}));
};
const scheduleViewportSync=()=>{if(!frame)frame=requestAnimationFrame(syncViewport)};
syncViewport();
window.visualViewport?.addEventListener('resize',scheduleViewportSync,{passive:true});
window.visualViewport?.addEventListener('scroll',scheduleViewportSync,{passive:true});
window.addEventListener('resize',scheduleViewportSync,{passive:true});
window.addEventListener('orientationchange',()=>setTimeout(scheduleViewportSync,120),{passive:true});
})();
document.addEventListener('submit',event=>{
const form=event.target;if(!form.matches('form'))return;
requestAnimationFrame(()=>{if(!form.checkValidity())return;form.querySelectorAll('button[type="submit"],button:not([type])').forEach(button=>{button.disabled=true;button.setAttribute('aria-busy','true');if(!button.dataset.label)button.dataset.label=button.textContent;button.textContent='در حال ثبت…'})});
+14 -7
View File
@@ -25,14 +25,15 @@
@media (prefers-reduced-motion:reduce){.live-dot:after{animation:none}.stat-card,.staff-ticket{transition:none}}
/* Focused ticket conversation workspace */
body:has(.ticket-workspace){overflow:hidden}.customer-main:has(.ticket-workspace),.staff-content:has(.ticket-workspace){max-width:none;padding:0}.ticket-workspace{height:calc(100dvh - 4.25rem);min-height:32rem;background:var(--color-soft);overflow:hidden}.ticket-workspace-head{@apply flex min-h-18 items-center justify-between gap-3 border-b border-ink/8 bg-paper px-3 py-2 sm:px-5}.ticket-title{@apply flex min-w-0 items-center gap-3}.ticket-title>div{@apply min-w-0}.ticket-title h1{@apply truncate text-base sm:text-xl}.workspace-back,.icon-button{@apply grid size-10 shrink-0 place-items-center rounded-xl border border-ink/8 bg-cream text-ink shadow-none transition hover:border-sea/30 hover:bg-mint}.ticket-head-actions{@apply flex shrink-0 items-center gap-2}.ticket-head-actions form{@apply m-0}.ticket-head-actions form button{@apply min-h-10 rounded-xl px-3 py-2}.workspace-grid{display:grid;height:calc(100% - 4.5rem);min-height:0}.customer-grid{grid-template-columns:minmax(0,1fr) 280px}.staff-conversation-grid{grid-template-columns:240px minmax(0,1fr) 240px}.conversation-pane{position:relative;display:grid;grid-template-rows:auto minmax(0,1fr) auto;min-width:0;min-height:0;background:var(--color-paper);border-inline:1px solid rgba(8,55,107,.07)}.conversation-toolbar{@apply flex min-h-12 items-center justify-between border-b border-ink/6 bg-paper px-4}.conversation-toolbar>div{@apply flex items-center gap-2 text-xs}.conversation-toolbar small{@apply text-[10px] text-ink/40}
body:has(.ticket-workspace){overflow:hidden;overscroll-behavior:none}.customer-main:has(.ticket-workspace),.staff-content:has(.ticket-workspace){max-width:none;padding:0}.ticket-workspace{display:grid;grid-template-rows:auto minmax(0,1fr);height:min(calc(100svh - 4.25rem),calc(var(--visual-viewport-height,100svh) - 4.25rem));min-height:0;background:var(--color-soft);overflow:hidden;overscroll-behavior:none}.ticket-workspace-head{@apply flex min-h-18 items-center justify-between gap-3 border-b border-ink/8 bg-paper px-3 py-2 sm:px-5}.ticket-title{@apply flex min-w-0 items-center gap-3}.ticket-title>div{@apply min-w-0}.ticket-title h1{@apply truncate text-base sm:text-xl}.workspace-back,.icon-button{@apply grid size-10 shrink-0 place-items-center rounded-xl border border-ink/8 bg-cream text-ink shadow-none transition hover:border-sea/30 hover:bg-mint}.ticket-head-actions{@apply flex shrink-0 items-center gap-2}.ticket-head-actions form{@apply m-0}.ticket-head-actions form button{@apply min-h-10 rounded-xl px-3 py-2}.workspace-grid{display:grid;height:auto;min-height:0}.customer-grid{grid-template-columns:minmax(0,1fr) 280px}.staff-conversation-grid{grid-template-columns:240px minmax(0,1fr) 240px}.conversation-pane{position:relative;display:grid;grid-template-rows:auto minmax(0,1fr) auto;min-width:0;min-height:0;background:var(--color-paper);border-inline:1px solid rgba(8,55,107,.07)}.conversation-toolbar{@apply flex min-h-12 items-center justify-between border-b border-ink/6 bg-paper px-4}.conversation-toolbar>div{@apply flex items-center gap-2 text-xs}.conversation-toolbar small{@apply text-[10px] text-ink/40}
.ticket-workspace .conversation{display:block;max-height:none;min-height:0;overflow-y:auto;overscroll-behavior:contain;scroll-behavior:smooth;border-radius:0;padding:1rem;background:linear-gradient(180deg,var(--color-soft),var(--color-cream));scrollbar-gutter:stable}.ticket-workspace .conversation:focus-visible{outline:3px solid color-mix(in srgb,var(--color-sea) 25%,transparent);outline-offset:-3px}.ticket-workspace .message{position:relative;display:flex;width:100%;max-width:none;gap:.625rem;border-radius:0;background:transparent;padding:0;box-shadow:none;margin:.65rem 0}.ticket-workspace .message.mine{flex-direction:row-reverse;background:transparent}.message-avatar{@apply grid size-8 shrink-0 place-items-center rounded-xl bg-ink text-[11px] font-black text-white}.message.mine .message-avatar{@apply bg-sea}.message.note .message-avatar{@apply bg-coral text-ink}.message-bubble{position:relative;max-width:min(78%,46rem);border:1px solid rgba(8,55,107,.07);border-radius:18px 5px 18px 18px;background:var(--color-paper);padding:.7rem .85rem;box-shadow:0 8px 24px -22px rgba(8,55,107,.65)}.message.mine .message-bubble{border-radius:5px 18px 18px 18px;background:var(--theme-own-message)}.message.note{justify-content:center}.message.note .message-bubble{width:min(92%,52rem);max-width:none;border:1px dashed color-mix(in srgb,var(--color-coral) 60%,transparent);border-radius:14px;background:var(--theme-note)}.message-bubble header{@apply mb-1.5 flex flex-wrap items-center gap-2}.message-bubble header time{@apply mr-auto}.internal-label{@apply rounded-full bg-coral/15 px-2 py-0.5 text-[9px] font-black text-ink}.message-bubble>p{@apply text-sm leading-7}.edited-label,.revision-link{@apply ml-2 text-[9px] text-ink/35}.revision-link{@apply font-bold text-sea}.deleted-message{@apply italic text-ink/40}.date-divider{@apply sticky top-1 z-10 my-4 flex items-center justify-center}.date-divider span{@apply rounded-full border border-ink/8 bg-paper/90 px-3 py-1 text-[9px] font-bold text-ink/45 shadow-sm backdrop-blur}
.attachment-list{@apply mt-3 grid gap-2}.attachment{margin-top:0}.attachment-image{@apply block max-w-72 overflow-hidden p-1}.attachment-image img{@apply max-h-52 w-full rounded-lg object-cover}.attachment-image span{@apply block truncate px-2 py-1 text-[10px]}.attachment-file{@apply flex max-w-sm items-center gap-3 bg-white/75}.file-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-mint text-lg}.attachment-file b,.attachment-file small{@apply block}.attachment-file small{@apply mt-0.5 text-[9px] font-normal text-ink/40}.attachment-audio{@apply block max-w-md}.attachment-audio audio{@apply my-2 h-10 w-full}.attachment-audio>a{@apply text-[10px]}.message-menu{position:absolute;top:.35rem;left:.35rem}.message.mine .message-menu{left:auto;right:.35rem}.message-menu>summary{@apply grid size-8 cursor-pointer list-none place-items-center rounded-full text-xs tracking-widest text-ink/40 transition hover:bg-white hover:text-sea;box-shadow:none}.message-menu>summary::-webkit-details-marker{display:none}.message-menu[open]>summary{@apply bg-white text-sea;box-shadow:0 5px 15px -10px #08376b}.message-menu-panel{@apply absolute left-0 z-20 mt-1 w-72 overflow-hidden rounded-2xl border border-ink/10 bg-paper shadow-xl}.message.mine .message-menu-panel{right:0;left:auto}.message-menu-head{@apply flex items-center justify-between border-b border-ink/8 bg-ink/[.025] px-4 py-3}.message-menu-head b,.message-menu-head small{@apply block}.message-menu-head b{@apply text-xs text-ink}.message-menu-head small{@apply mt-0.5 text-[9px] font-normal text-ink/45}.message-menu-close{@apply grid size-8 min-h-0 place-items-center rounded-full bg-transparent p-0 text-lg font-normal text-ink/45 shadow-none hover:bg-ink/5 hover:text-ink}.message-edit-form{@apply p-4}.message-edit-form label{@apply mb-1.5 text-[10px] text-ink/65}.message-menu-panel textarea{@apply min-h-24 rounded-xl bg-ink/[.025] p-3 text-xs leading-6 shadow-none}.message-menu-actions{@apply mt-3 grid grid-cols-2 gap-2}.message-menu-actions button{@apply m-0 min-h-10 w-full rounded-xl px-3 py-2 text-xs}.message-edit-save{@apply bg-sea}.message-edit-cancel{@apply border border-ink/10 bg-white text-ink/60 shadow-none hover:bg-ink/5 hover:text-ink}.message-delete-zone{@apply flex items-center justify-between gap-3 border-t border-red-200 bg-red-50/50 px-4 py-3}.message-delete-zone b,.message-delete-zone small{@apply block}.message-delete-zone b{@apply text-[10px] text-red-700}.message-delete-zone small{@apply mt-0.5 text-[9px] text-red-700/60}.message-delete-zone form{@apply m-0}.message-delete-button{@apply m-0 min-h-8 rounded-lg border border-red-200 bg-white px-3 py-1 text-[10px] text-red-700 shadow-none hover:bg-red-500 hover:text-white}@media(max-width:639px){.message-menu-panel{position:fixed;right:.75rem!important;bottom:5rem;left:.75rem!important;top:auto;width:auto;max-height:calc(100dvh - 7rem);overflow:auto}}.conversation-empty{@apply grid h-full min-h-64 place-content-center text-center}.conversation-empty>span{@apply mx-auto mb-3 grid size-12 place-items-center rounded-full bg-mint text-sea}.conversation-empty p{@apply mt-1 text-xs text-ink/45}
.ticket-workspace .composer{position:relative;z-index:15;margin:0;border:0;border-top:1px solid rgba(8,55,107,.08);border-radius:0;padding:.65rem .85rem max(.65rem,env(safe-area-inset-bottom));box-shadow:0 -14px 40px -30px rgba(8,55,107,.55)}.composer-context{@apply mb-2 flex items-start justify-between}.composer-context b,.composer-context small{@apply block}.composer-context b{@apply text-xs}.composer-context small,.composer-context>[data-draft-status]{@apply mt-0.5 text-[9px] text-ink/40}.ticket-workspace .composer textarea{max-height:9rem;min-height:2.75rem;resize:none;border-radius:13px;background:var(--color-cream);padding:.7rem .85rem;line-height:1.7}.ticket-workspace input[type=file]{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.composer-bar{@apply mt-2 flex items-center gap-2}.composer-tools{@apply flex items-center gap-1}.tool-button{@apply mb-0 inline-flex min-h-10 cursor-pointer items-center gap-1 rounded-xl border border-ink/8 bg-paper px-3 text-xs font-bold text-ink shadow-none hover:border-sea/30 hover:bg-mint}.tool-button span{@apply hidden sm:inline}.composer-hint{@apply hidden flex-1 text-center text-[9px] text-ink/30 lg:block}.send-button{@apply mr-auto min-h-10 rounded-xl bg-sea px-4 py-2}.compact-recorder{@apply my-2 rounded-xl p-3}.compact-recorder[hidden],.composer-extra[hidden]{display:none}.composer-extra{@apply mt-2 rounded-xl border border-ink/8 bg-cream p-3}.composer-extra label{@apply text-xs}.new-message-jump{@apply absolute bottom-36 left-1/2 z-20 min-h-9 -translate-x-1/2 rounded-full bg-ink px-4 py-2 text-[10px] shadow-xl}.new-message-jump[hidden]{display:none}.staff-composer .mode{@apply mb-2}.staff-composer.note-mode{background:var(--theme-note);border-top-color:color-mix(in srgb,var(--color-coral) 45%,transparent)}.staff-composer.note-mode textarea{@apply bg-white}.staff-composer.note-mode .send-button{@apply bg-coral text-ink}.file-preview:empty{display:none}.ticket-workspace .file-preview{@apply mb-2 mt-2 grid grid-cols-2 gap-2 sm:grid-cols-4}.ticket-workspace .file-chip{@apply text-[9px]}
.workspace-side{min-height:0;overflow-y:auto;padding:.75rem;background:var(--color-cream)}.workspace-side .info-card{@apply mb-3 rounded-[14px] p-4 shadow-none}.panel-close{@apply hidden}.ticket-operations form{@apply mt-3 border-t border-ink/6 pt-3}.ticket-operations form button{@apply w-full}.ticket-operations details{@apply mt-3 rounded-xl border border-ink/8 p-3}.ticket-operations summary{@apply cursor-pointer text-xs font-bold}
.sms-delivery-heading{display:flex;align-items:center;justify-content:space-between;gap:.75rem}.sms-delivery-heading h3,.sms-delivery-heading small{display:block}.sms-delivery-heading small{margin-top:.2rem;color:rgba(8,55,107,.48);font-size:.6rem}.sms-delivery-heading>span{display:grid;width:1.75rem;height:1.75rem;flex:none;place-items:center;border-radius:999px;background:var(--color-mint);color:var(--color-sea);font-weight:900}.sms-delivery-list{display:grid;gap:.45rem;margin-top:.75rem}.sms-delivery-item{border:1px solid rgba(8,55,107,.09);border-radius:.75rem;background:var(--color-paper);overflow:hidden}.sms-delivery-item>summary{display:grid;grid-template-columns:minmax(0,1fr) auto 1.6rem;align-items:center;gap:.45rem;min-height:2.8rem;padding:.5rem .6rem;cursor:pointer;list-style:none}.sms-delivery-item>summary::-webkit-details-marker{display:none}.sms-delivery-event{min-width:0;overflow:hidden;color:rgba(8,55,107,.68);font-size:.65rem;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.sms-delivery-item>summary b{font-size:.64rem;color:var(--color-ink)}.sms-delivery-info{display:grid;width:1.55rem;height:1.55rem;place-items:center;border:1px solid rgba(6,147,227,.25);border-radius:999px;background:#eef8ff;color:var(--color-sea);font-size:.68rem;font-weight:900}.sms-delivery-item[open] .sms-delivery-info{background:var(--color-sea);color:#fff}.sms-delivery-timeline{display:grid;gap:.7rem;margin:0;padding:.7rem .75rem .75rem;list-style:none;border-top:1px solid rgba(8,55,107,.07);background:var(--color-cream)}.sms-delivery-timeline li{display:grid;grid-template-columns:.65rem minmax(0,1fr);gap:.55rem;align-items:start;color:rgba(8,55,107,.48)}.sms-delivery-timeline li>span{width:.55rem;height:.55rem;margin-top:.28rem;border:2px solid #aab9c6;border-radius:999px;background:#fff}.sms-delivery-timeline li.is-complete>span{border-color:var(--color-sea);background:var(--color-sea)}.sms-delivery-timeline b,.sms-delivery-timeline time,.sms-delivery-timeline small{display:block}.sms-delivery-timeline b{font-size:.62rem}.sms-delivery-timeline time,.sms-delivery-timeline small{margin-top:.12rem;font-size:.58rem;color:rgba(8,55,107,.58)}.sms-delivery-timeline li.is-complete b{color:var(--color-ink)}.sms-delivery-description,.sms-delivery-error,.sms-delivery-tracking{display:block;margin:0;padding:.55rem .75rem;border-top:1px solid rgba(8,55,107,.07);font-size:.58rem;line-height:1.75}.sms-delivery-description{color:#526b85}.sms-delivery-error{color:#8e2d2d;background:#fff2f2}.sms-delivery-tracking{overflow-wrap:anywhere;color:#647b91;text-align:left}
@media(max-width:1023px){body:has(.ticket-workspace) .mobile-dock,body:has(.ticket-workspace) .staff-mobile-dock{display:none}.ticket-workspace{height:calc(100dvh - 4.25rem)}.staff-conversation-grid,.customer-grid{grid-template-columns:minmax(0,1fr)}.workspace-side{position:fixed;inset:4.25rem 0 0 auto;z-index:60;width:min(88vw,22rem);transform:translateX(105%);border-left:1px solid rgba(8,55,107,.1);box-shadow:-20px 0 60px -35px rgba(8,55,107,.7);transition:transform .25s ease}.workspace-side.panel-open{transform:translateX(0)}.panel-close{@apply mb-3 grid size-10 place-items-center rounded-xl bg-ink text-white}.message-bubble{max-width:86%}}
@media(max-width:639px){.site-header{display:none}.customer-main:has(.ticket-workspace){padding:0}.ticket-workspace{height:100dvh}.ticket-workspace-head{@apply min-h-16}.workspace-grid{height:calc(100% - 4rem)}.ticket-title h1{@apply max-w-[48vw] text-sm}.ticket-head-actions .badge{@apply max-w-24 truncate}.conversation-toolbar{@apply min-h-10}.ticket-workspace .conversation{@apply px-2.5 py-3}.message-avatar{@apply size-7 rounded-lg}.message-bubble{max-width:88%;padding:.6rem .7rem}.message.note .message-bubble{width:96%}.ticket-workspace .composer{@apply px-2.5 pt-2}.composer-context small{max-width:70vw}.composer-hint{display:none}.send-button{@apply px-3}.new-message-jump{bottom:8.5rem}.ticket-head-actions form{display:none}}
@media(max-width:1023px){body:has(.ticket-workspace) .mobile-dock,body:has(.ticket-workspace) .staff-mobile-dock{display:none}.staff-conversation-grid,.customer-grid{grid-template-columns:minmax(0,1fr)}.workspace-side{position:fixed;top:calc(var(--visual-viewport-offset-top,0px) + 4.25rem);right:0;bottom:0;z-index:60;width:min(88vw,22rem);transform:translateX(105%);overflow-y:auto;overscroll-behavior:contain;border-left:1px solid rgba(8,55,107,.1);box-shadow:-20px 0 60px -35px rgba(8,55,107,.7);transition:transform .25s ease}.workspace-side.panel-open{transform:translateX(0)}.panel-close{@apply mb-3 grid size-10 place-items-center rounded-xl bg-ink text-white}.message-bubble{max-width:86%}}
@media(max-width:1023px){.ticket-workspace .composer{min-width:0;max-height:min(45svh,max(7rem,calc(var(--visual-viewport-height,100svh) - 18rem)),28rem);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}}
@media(max-width:639px){.site-header{display:none}.customer-main:has(.ticket-workspace){padding:0}.ticket-workspace{height:min(100svh,var(--visual-viewport-height,100svh))}.staff-ticket-workspace{height:min(calc(100svh - 4.5rem),calc(var(--visual-viewport-height,100svh) - 4.5rem))}.ticket-workspace-head{@apply min-h-16}.workspace-grid{height:auto}.ticket-title h1{@apply max-w-[48vw] text-sm}.ticket-head-actions .badge{@apply max-w-24 truncate}.conversation-toolbar{@apply min-h-10}.ticket-workspace .conversation{@apply px-2.5 py-3}.message-avatar{@apply size-7 rounded-lg}.message-bubble{max-width:88%;padding:.6rem .7rem}.message.note .message-bubble{width:96%}.ticket-workspace .composer{@apply px-2.5 pt-2}.composer-context small{max-width:70vw}.composer-hint{display:none}.send-button{@apply px-3}.new-message-jump{bottom:8.5rem}.ticket-head-actions form{display:none}.workspace-side{top:var(--visual-viewport-offset-top,0px)}}
@media(prefers-reduced-motion:reduce){.ticket-workspace .conversation{scroll-behavior:auto}.workspace-side{transition:none}}
.load-older{@apply mx-auto my-3 flex min-h-9 rounded-full border border-ink/10 bg-paper px-4 py-2 text-[10px] font-bold text-sea shadow-sm hover:bg-mint}.load-older.htmx-request{@apply pointer-events-none opacity-60}
@@ -42,9 +43,9 @@ body:has(.ticket-workspace){overflow:hidden}.customer-main:has(.ticket-workspace
/* Collapsible reply panel keeps the correspondence history dominant. */
.ticket-workspace .composer.is-collapsed{padding:.55rem .85rem;border-top:1px solid rgba(8,55,107,.1);background:var(--color-paper)}.composer.is-collapsed>form,.composer.is-collapsed>.mode{display:none}.composer-launch{display:none;width:100%;min-height:3.5rem;align-items:center;justify-content:flex-start;gap:.75rem;border:1px solid rgba(8,55,107,.1);border-radius:11px;background:linear-gradient(135deg,var(--color-paper),var(--color-mint));padding:.55rem .75rem;color:var(--color-ink);box-shadow:none;text-align:right}.composer.is-collapsed .composer-launch{display:flex}.composer-launch:hover{transform:none;border-color:color-mix(in srgb,var(--color-sea) 35%,transparent);background:var(--color-mint)}.composer-launch-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-sea text-lg text-white}.composer-launch b,.composer-launch small{display:block}.composer-launch b{@apply text-xs}.composer-launch small{@apply mt-0.5 text-[9px] font-medium text-ink/45}.composer-launch-arrow{@apply mr-auto text-sm text-sea}.composer-close{@apply text-ink/55}.composer:not(.is-collapsed) .composer-launch{display:none}.composer.is-collapsed~*{transition:none}.composer.is-collapsed+.new-message-jump{bottom:5rem}.ticket-workspace .composer.is-collapsed{box-shadow:0 -10px 30px -28px rgba(8,55,107,.65)}
@media(max-width:639px){.ticket-workspace .composer.is-collapsed{@apply px-2.5 py-2}.composer-launch{min-height:3.25rem}.composer-launch small{max-width:65vw}.new-message-jump{bottom:5.5rem}.ticket-workspace .composer{min-width:0;max-height:min(45dvh,28rem);overflow-y:auto;overscroll-behavior:contain}.ticket-workspace .composer-bar{position:sticky;bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));z-index:2;margin-inline:-.65rem;margin-bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));padding:.75rem .65rem max(1rem,env(safe-area-inset-bottom));background:var(--color-paper)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools{display:grid;grid-template-columns:1fr 1fr 1fr;width:100%}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools .tool-button{justify-content:center;min-width:0;padding-inline:.4rem}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-close{display:inline-flex;order:initial;color:var(--color-ink)} }
@media(max-width:639px){.ticket-workspace .composer.is-collapsed{@apply px-2.5 py-2}.composer-launch{min-height:3.25rem}.composer-launch small{max-width:65vw}.new-message-jump{bottom:5.5rem}.ticket-workspace .composer{min-width:0;max-height:min(45svh,max(7rem,calc(var(--visual-viewport-height,100svh) - 18rem)),28rem);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}.ticket-workspace .composer-bar{position:sticky;bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));z-index:2;margin-inline:-.65rem;margin-bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));padding:.75rem .65rem max(1rem,env(safe-area-inset-bottom));background:var(--color-paper)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools{display:grid;grid-template-columns:1fr 1fr 1fr;width:100%}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools .tool-button{justify-content:center;min-width:0;padding-inline:.4rem}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-close{display:inline-flex;order:initial;color:var(--color-ink)} }
.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:17rem}@media(max-width:639px){.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5.5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:19rem}}
body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;width:min(calc(100% - 2rem),42rem);margin:0;transform:translateX(-50%);box-shadow:0 18px 50px -25px rgba(8,55,107,.55);animation:flash-in .22s ease-out}.flash{position:relative;padding-left:3rem}.flash-close{position:absolute;left:.5rem;top:50%;min-height:2rem;width:2rem;transform:translateY(-50%);border-radius:.5rem;background:transparent;padding:0;color:currentColor;box-shadow:none}.flash-close:hover{transform:translateY(-50%);background:rgba(255,255,255,.6)}.flash-leaving{opacity:0;transform:translate(-50%,-.5rem)!important;transition:.25s ease}@keyframes flash-in{from{opacity:0;transform:translate(-50%,-.5rem)}to{opacity:1;transform:translate(-50%,0)}}
body:has(.ticket-workspace) .flash{position:fixed;top:calc(var(--visual-viewport-offset-top,0px) + 5rem);left:50%;z-index:100;width:min(calc(100% - 2rem),42rem);margin:0;transform:translateX(-50%);box-shadow:0 18px 50px -25px rgba(8,55,107,.55);animation:flash-in .22s ease-out}.flash{position:relative;padding-left:3rem}.flash-close{position:absolute;left:.5rem;top:50%;min-height:2rem;width:2rem;transform:translateY(-50%);border-radius:.5rem;background:transparent;padding:0;color:currentColor;box-shadow:none}.flash-close:hover{transform:translateY(-50%);background:rgba(255,255,255,.6)}.flash-leaving{opacity:0;transform:translate(-50%,-.5rem)!important;transition:.25s ease}@keyframes flash-in{from{opacity:0;transform:translate(-50%,-.5rem)}to{opacity:1;transform:translate(-50%,0)}}
/* Group context adds a row above the conversation; keep every control inside the viewport. */
.staff-ticket-workspace:has(.ticket-dispatch-context){display:grid;grid-template-rows:auto auto minmax(0,1fr)}.staff-ticket-workspace:has(.ticket-dispatch-context) .workspace-grid{height:auto}.ticket-dispatch-context{margin:.5rem .65rem 0}
@@ -91,9 +92,11 @@ body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;
.ticket-company-logo{display:block;width:3.25rem;height:auto;max-height:1.45rem;flex-shrink:0;object-fit:contain}
.site-footer{display:flex;min-height:2rem;align-items:center;justify-content:center;gap:.35rem;padding:.4rem 1rem;color:#60758b;font-size:.68rem;font-weight:500;text-align:center}
.staff-main>.site-footer{border-top:1px solid rgba(8,55,107,.06)}
body:has(.ticket-workspace) .site-footer{display:none}body:has(.ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.25rem)}body:has(.staff-ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.5rem)}
body:has(.ticket-workspace) .site-footer{display:none}body:has(.ticket-workspace) .ticket-workspace{height:min(calc(100svh - 4.25rem),calc(var(--visual-viewport-height,100svh) - 4.25rem))}body:has(.staff-ticket-workspace) .ticket-workspace{height:min(calc(100svh - 4.5rem),calc(var(--visual-viewport-height,100svh) - 4.5rem))}
.customer-workspace.has-ticket-detail-credit{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.customer-workspace.has-ticket-detail-credit .workspace-grid{height:auto;min-height:0}.ticket-detail-credit{display:flex;min-height:2rem;align-items:center;justify-content:center;border-top:1px solid rgba(8,55,107,.08);background:var(--color-paper);padding:.35rem 1rem;color:#60758b;font-size:.68rem;font-weight:500;line-height:1.6;text-align:center}.ticket-detail-credit a{color:var(--color-sea)}
@media(max-width:639px){.logo-copy{font-size:.82rem}.header-company-logo{width:4.75rem;max-height:2.1rem}.auth-brand{gap:.5rem}.auth-brand img{width:7rem;max-height:3.1rem}.auth-brand>div{min-width:0}.auth-footer{font-size:.6rem}.ticket-company-logo{width:2.5rem;max-height:1.1rem}.site-footer{padding-inline:3.5rem;font-size:.62rem;line-height:1.6}body:has(.ticket-workspace) .ticket-workspace{height:100dvh}body:has(.staff-ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.5rem)}}
@media(max-width:639px){.logo-copy{font-size:.82rem}.header-company-logo{width:4.75rem;max-height:2.1rem}.auth-brand{gap:.5rem}.auth-brand img{width:7rem;max-height:3.1rem}.auth-brand>div{min-width:0}.auth-footer{font-size:.6rem}.ticket-company-logo{width:2.5rem;max-height:1.1rem}.site-footer{padding-inline:3.5rem;font-size:.62rem;line-height:1.6}body:has(.ticket-workspace) .ticket-workspace{height:min(100svh,var(--visual-viewport-height,100svh))}body:has(.staff-ticket-workspace) .ticket-workspace{height:min(calc(100svh - 4.5rem),calc(var(--visual-viewport-height,100svh) - 4.5rem))}body:has(.ticket-workspace) .flash{top:calc(var(--visual-viewport-offset-top,0px) + .75rem)}}
@media(display-mode:standalone){body:has(.ticket-workspace) .ticket-workspace{height:min(100dvh,var(--visual-viewport-height,100dvh))}body:has(.staff-ticket-workspace) .ticket-workspace{height:min(calc(100dvh - 4.5rem),calc(var(--visual-viewport-height,100dvh) - 4.5rem))}}
/* Session actions, staff-created tickets and install prompt */
.desktop-nav form,.staff-topbar-actions form{margin:0}.logout-button{min-height:auto;border:0;background:transparent;padding:.5rem .75rem;color:var(--color-coral);box-shadow:none}.logout-button:hover{transform:none;background:#fff;color:#b45309;box-shadow:none}
@@ -136,5 +139,9 @@ body:has(.ticket-workspace) .site-footer{display:none}body:has(.ticket-workspace
.chat-coach [data-chat-coach-dismiss]:hover{border-color:rgba(217,119,6,.4);background:rgba(251,191,36,.22);color:#78350f}
.chat-coach [data-chat-coach-dismiss]:focus-visible{outline:2px solid #d97706;outline-offset:2px}
/* Legacy fallback: VisualViewport still avoids the 100vh browser-toolbar defect
on engines that do not yet parse the small viewport unit. */
@supports not (height:100svh){body:has(.ticket-workspace) .ticket-workspace{height:calc(var(--visual-viewport-height,100vh) - 4.25rem)}body:has(.staff-ticket-workspace) .ticket-workspace{height:calc(var(--visual-viewport-height,100vh) - 4.5rem)}@media(max-width:639px){body:has(.ticket-workspace) .ticket-workspace{height:var(--visual-viewport-height,100vh)}body:has(.staff-ticket-workspace) .ticket-workspace{height:calc(var(--visual-viewport-height,100vh) - 4.5rem)}}}
/* Compact message actions: lower-left trigger, two choices, focused editor. */
.message-menu{top:auto!important;right:auto!important;bottom:.35rem!important;left:.35rem!important}.message-menu-panel{top:auto;right:auto!important;bottom:calc(100% + .35rem);left:0!important;margin:0;width:15rem}.message-menu-choices{@apply grid gap-1 p-1.5}.message-menu-choices form{@apply m-0}.message-choice{@apply m-0 flex min-h-12 w-full items-center justify-start gap-2.5 rounded-xl bg-transparent px-3 py-2 text-right text-ink shadow-none hover:bg-mint hover:text-sea}.message-choice>span:nth-child(2){@apply min-w-0 flex-1}.message-choice b,.message-choice small{@apply block}.message-choice b{@apply text-xs}.message-choice small{@apply mt-0.5 text-[9px] font-medium text-ink/45}.message-choice-icon{@apply grid size-8 shrink-0 place-items-center rounded-lg bg-mint text-base text-sea}.message-choice-arrow{@apply text-lg text-ink/25}.message-choice-delete{@apply text-red-700 hover:bg-red-50 hover:text-red-700}.message-choice-delete .message-choice-icon{@apply bg-red-50 text-red-700}.message-editor-back{@apply grid size-8 min-h-0 place-items-center rounded-full bg-transparent p-0 text-base text-ink/50 shadow-none hover:bg-ink/5 hover:text-ink}.message-edit-meta{@apply mt-1.5 flex justify-between gap-2 text-[8px] text-ink/40}@media(max-width:639px){.message-menu-panel{top:auto!important;right:.75rem!important;bottom:5rem!important;left:.75rem!important;width:auto}.message-menu-choices{@apply p-2}.message-choice{@apply min-h-14}}
+1 -1
View File
@@ -2,7 +2,7 @@
<!doctype html>
<html lang="fa" dir="rtl" data-static-version="{{ static_asset_version }}">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#08376b">
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,interactive-widget=resizes-content"><meta name="theme-color" content="#08376b">
<meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="{{ site_branding.pwa_title }}">
<title>{% block title %}{{ page_title }}{% endblock %}</title>
<link rel="icon" href="{{ favicon_url }}?v={{ static_asset_version }}-{{ branding_version }}">
+103
View File
@@ -0,0 +1,103 @@
import pytest
from django.urls import reverse
from playwright.sync_api import expect
from apps.accounts.models import TicketMagicLink, User
from apps.core.models import SiteBranding
from apps.customers.models import CustomerProfile
from apps.messaging.models import Message
from apps.staff.models import StaffProfile
from apps.tickets.models import TicketCategory
from apps.tickets.services import create_ticket
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.tickets import TicketConversationPage
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
PASSWORD = "E2e-safe-password-2026"
def assert_workspace_fits_visual_viewport(page):
metrics = page.evaluate(
"""() => {
const box = document.querySelector('.ticket-workspace').getBoundingClientRect();
const viewportHeight = window.visualViewport?.height || window.innerHeight;
return {
top: box.top,
bottom: box.bottom,
viewportHeight,
pageWidth: document.documentElement.scrollWidth,
viewportWidth: document.documentElement.clientWidth,
};
}"""
)
assert metrics["top"] >= -1
assert metrics["bottom"] <= metrics["viewportHeight"] + 1
assert metrics["pageWidth"] <= metrics["viewportWidth"] + 1
def test_magic_link_mobile_viewport_keeps_composer_messages_and_credit_visible(live_server, browser_page):
branding = SiteBranding.load()
branding.developer_credit_text = "امضای مقدس توسعه‌دهنده E2E"
branding.save()
category = TicketCategory.objects.create(title="موبایل لینک امن", slug="magic-mobile-viewport")
customer = User.objects.create_user(username="magic-mobile", mobile="09120009101", account_type=User.AccountType.CUSTOMER)
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="لینک امن")
staff = User.objects.create_user(username="magic-mobile-staff", mobile="09120009102", account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff, display_name="کارشناس لینک امن")
ticket = create_ticket(customer=customer, category=category, subject="بررسی موبایل لینک امن", body="پیام آغازین")
for index in range(12):
Message.objects.create(ticket=ticket, author=staff, body=f"پاسخ کارشناس شماره {index + 1}")
token = TicketMagicLink.issue(ticket)
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 844})
page.goto(f"{live_server.url}{reverse('accounts:magic', args=[token])}", wait_until="domcontentloaded")
conversation = TicketConversationPage(page, live_server.url)
credit = page.locator(".ticket-detail-credit")
expect(credit).to_be_visible()
expect(credit).to_contain_text("امضای مقدس توسعه‌دهنده E2E")
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
conversation.open_composer()
page.set_viewport_size({"width": 390, "height": 620})
expect(page.locator('form[data-ticket-composer] textarea')).to_be_focused()
expect(credit).to_be_visible()
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
page.locator("[data-composer-close]").click()
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
expect(credit).to_be_visible()
assert_workspace_fits_visual_viewport(page)
def test_staff_ticket_mobile_viewport_survives_open_close_and_rotation(live_server, browser_page):
category = TicketCategory.objects.create(title="موبایل کارشناس", slug="staff-mobile-viewport")
customer = User.objects.create_user(username="staff-mobile-customer", mobile="09120009103", account_type=User.AccountType.CUSTOMER)
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="پنل کارشناس")
staff = User.objects.create_user(username="staff-mobile-user", mobile="09120009104", password=PASSWORD, account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff, display_name="کارشناس موبایل")
ticket = create_ticket(customer=customer, category=category, subject="بررسی موبایل پنل کارشناس", selected_expert=staff, body="شرح درخواست مشتری")
for index in range(12):
Message.objects.create(ticket=ticket, author=customer, body=f"پیام مشتری شماره {index + 1}")
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 844})
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
conversation = TicketConversationPage(page, live_server.url)
conversation.goto(reverse("staff_tickets:detail", args=[ticket.public_id]))
expect(page.get_by_role("button", name="ثبت اقدام جدید در تیکت")).to_be_visible()
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
conversation.open_composer()
page.set_viewport_size({"width": 844, "height": 390})
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
expect(page.locator("[data-composer-close]")).to_be_visible()
page.locator("[data-composer-close]").click()
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
assert_workspace_fits_visual_viewport(page)
+11 -3
View File
@@ -136,9 +136,17 @@ def test_e2e_subject_stays_empty_for_manual_and_search_routing_until_user_types(
subject_input = page.locator("[data-ticket-subject]")
page.locator(".ticket-manual-routing summary").click()
expect(form.unit).to_be_visible()
unit_box = form.unit.bounding_box()
category_box = form.category.bounding_box()
assert unit_box and category_box
boxes = page.locator("[data-routing-selectors]").evaluate(
"""root => {
const unit = root.querySelector('[data-ticket-unit]').getBoundingClientRect();
const category = root.querySelector('[data-ticket-category]').getBoundingClientRect();
return {
unit: {x: unit.x, y: unit.y, width: unit.width},
category: {x: category.x, y: category.y, width: category.width},
};
}"""
)
unit_box, category_box = boxes["unit"], boxes["category"]
assert abs(unit_box["y"] - category_box["y"]) < 2
assert unit_box["x"] + unit_box["width"] <= category_box["x"] or category_box["x"] + category_box["width"] <= unit_box["x"]
expect(subject_input).to_have_value("")
+19 -1
View File
@@ -442,10 +442,27 @@ def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably()
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
assert ".staff-ticket-workspace .composer.is-collapsed{position:fixed" not in source_css
assert ".staff-ticket-workspace .composer:not(.is-collapsed){position:fixed" not in source_css
assert "max-height:min(45dvh,28rem)" in source_css
assert "max-height:min(45svh,max(7rem,calc(var(--visual-viewport-height,100svh) - 18rem)),28rem)" in source_css
for menu_feature in ("data-message-menu","data-message-menu-close","closeMessageMenu","event.key==='Escape'","!menu.contains(event.target)"):
assert menu_feature in script or menu_feature in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
def test_ticket_workspace_uses_mobile_viewport_and_keyboard_safe_layout_contract():
base=Path("templates/base.html").read_text(encoding="utf-8")
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
ui_script=Path("static/js/ui.js").read_text(encoding="utf-8")
conversation_script=Path("static/js/conversation.js").read_text(encoding="utf-8")
magic_template=Path("templates/tickets/customer_detail.html").read_text(encoding="utf-8")
assert "interactive-widget=resizes-content" in base
for feature in ("100svh","--visual-viewport-height","overscroll-behavior:none","display-mode:standalone"):
assert feature in source_css
assert "min-height:32rem" not in source_css
assert "max-height:min(45svh,max(7rem,calc(var(--visual-viewport-height,100svh) - 18rem)),28rem)" in source_css
for feature in ("window.visualViewport","--visual-viewport-height","appviewportchange","orientationchange"):
assert feature in ui_script
assert "layoutObserver?.observe(composer)" in conversation_script
assert "focus({preventScroll:true})" in conversation_script
assert '<footer class="ticket-detail-credit">{{ developer_credit }}</footer>' in magic_template
def test_frontend_runtime_libraries_are_local_and_versioned():
template=Path("templates/base.html").read_text(encoding="utf-8")
assert "unpkg.com" not in template
@@ -527,6 +544,7 @@ def test_pwa_prompt_is_mobile_only_and_uses_square_app_icon(client):
script=Path("static/js/pwa.js").read_text(encoding="utf-8")
manifest=client.get(reverse("web_manifest")).json()
assert "mobileDevice" in script and "banner&&mobileDevice" in script
assert "hasWorkerController=Boolean(navigator.serviceWorker.controller)" in script
assert all(icon["src"].endswith("pwa-icon.png") for icon in manifest["icons"])