/*! 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-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-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial}}}@layer theme{:root,:host{--font-sans:-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-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-card:#fff;--color-line:#dde3ea}}@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-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{*{box-sizing:border-box}html{-webkit-text-size-adjust:100%}body{background:var(--bg);color:var(--ink);font-variant-numeric:tabular-nums;margin:0;font:400 15px/1.55 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.shell{grid-template-columns:var(--side-w) minmax(0, 1fr);min-height:100dvh;display:grid}.side{background:var(--brand-ink);color:#fff;flex-direction:column;gap:22px;padding:18px 12px 28px;display:flex}.brand{color:#fff;align-items:center;gap:10px;padding:4px 8px;text-decoration:none;display:flex}.brand-mark{letter-spacing:.4px;background:#ffffff21;border-radius:8px;flex:none;place-items:center;width:32px;height:32px;font-size:12.5px;font-weight:700;display:grid}.brand-text{letter-spacing:.1px;font-size:16px;font-weight:600}.brand-text small{text-transform:uppercase;letter-spacing:1px;opacity:.72;border:1px solid #ffffff4d;border-radius:4px;margin-left:6px;padding:1px 5px;font-size:9.5px;font-weight:600}.sidenav{flex:1}.navgroup{letter-spacing:.9px;text-transform:uppercase;color:#ffffff6b;margin:0 0 6px;padding:0 8px;font-size:10.5px;font-weight:700}.sidenav ul{margin:0 0 18px;padding:0;list-style:none}.sidenav li{margin-bottom:1px}.sidenav a{border-radius:var(--radius-s);color:#ffffffc7;padding:8px 10px;font-size:14px;text-decoration:none;transition:background .12s,color .12s;display:block}.sidenav a:hover{color:#fff;background:#ffffff17}.sidenav a[aria-current=page]{color:#fff;background:#ffffff24;font-weight:600;box-shadow:inset 2px 0 #ffffffa6}.sidenav a:focus-visible{outline-offset:-2px;outline:2px solid #fff}.sidefoot{padding:0 10px;border-top:1px solid #ffffff1a;padding-top:14px}.firmname{color:#ffffff8c;margin:0;font-size:12.5px;line-height:1.4}.main{flex-direction:column;min-width:0;display:flex}@media (min-width:901px){.shell{height:100dvh;overflow:hidden}.side,.main{overscroll-behavior:contain;overflow-y:auto}.side{scrollbar-width:thin;scrollbar-color:#ffffff47 transparent}.side::-webkit-scrollbar{width:10px}.side::-webkit-scrollbar-thumb{background:#ffffff3d padding-box content-box;border:3px solid #0000;border-radius:6px}.side::-webkit-scrollbar-thumb:hover{background:#fff6 padding-box content-box}.main{scrollbar-gutter:stable}.navdrawer-toggle,.navdrawer-summary{display:none}}.topbar{background:var(--card);border-bottom:1px solid var(--line);flex-wrap:wrap;align-items:center;gap:16px;padding:14px 28px;display:flex}.gsearch{flex:260px;min-width:0}.gsearch input{width:100%;max-width:460px;font:inherit;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:8px 12px;font-size:14px}.gsearch input:focus{outline-offset:1px;border-color:var(--brand-2);background:#fff;outline:2px solid #1b54884d}.who{align-items:center;gap:10px;margin-left:auto;font-size:13px;display:flex}.who-name{color:var(--ink-2);font-weight:550}.signout{color:var(--muted);border-bottom:1px solid var(--line);text-decoration:none}.signout:hover{color:var(--ink);border-color:var(--muted)}.page{flex:1;min-width:0;padding:26px 28px 56px}.page-head{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:22px;display:flex}.page-head h1{letter-spacing:-.2px;margin:0;font-size:23px;font-weight:650}.page-head .sub{color:var(--muted);margin:5px 0 0;font-size:14px}.page-actions{flex-wrap:wrap;gap:8px;display:flex}h2{margin:0 0 14px;font-size:16.5px;font-weight:650}.muted{color:var(--muted)}.card{margin-bottom:calc(var(--spacing) * 4);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line);background-color:var(--color-card);padding:calc(var(--spacing) * 5);border-radius:10px;box-shadow:0 1px 2px #141c280d,0 8px 20px -6px #141c2817}.card-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.card-head h2{margin:0}.grid{gap:16px;display:grid}.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.stat .n{letter-spacing:-.5px;font-size:27px;font-weight:650;line-height:1.1}.stat .l{color:var(--muted);margin-top:4px;font-size:13px}.stat.bad .n{color:var(--bad)}.stat.warn .n{color:var(--warn)}.stat.ok .n{color:var(--ok)}.table-scroll{-webkit-overflow-scrolling:touch;min-width:0;overflow-x:auto}table{border-collapse:collapse;width:100%;min-width:640px;font-size:14px}th,td{text-align:left;border-bottom:1px solid var(--line-soft);vertical-align:top;padding:10px 12px}th{text-transform:uppercase;letter-spacing:.5px;color:var(--muted);white-space:nowrap;font-size:11px;font-weight:700}tbody tr:hover{background:#fafbfc}td.num,th.num{text-align:right;white-space:nowrap}tbody tr:last-child td{border-bottom:0}.doc-ai-row[hidden]{display:none}tbody tr:has(+ .doc-ai-row[hidden]:last-child) td{border-bottom:0}.doc-ai-row td{padding-top:0}td a,.defs a{color:var(--brand-2);font-weight:550;text-decoration:none}td a:hover,.defs a:hover{text-decoration:underline}.pill{--tw-font-weight:var(--font-weight-bold);font-size:11px;font-weight:var(--font-weight-bold);--tw-tracking:.2px;letter-spacing:.2px;white-space:nowrap;background:var(--line-soft);color:var(--ink-2);border-radius:3.40282e38px;padding:2px 8px;display:inline-block}.pill.ok{background:var(--ok-bg);color:var(--ok)}.pill.warn{background:var(--warn-bg);color:var(--warn)}.pill.bad{background:var(--bad-bg);color:var(--bad)}.field{margin-bottom:16px}label{color:var(--ink-2);margin-bottom:6px;font-size:13px;font-weight:650;display:block}label .req{color:var(--bad)}input[type=text],input[type=email],input[type=password],input[type=date],input[type=time],input[type=tel],input[type=url],input[type=number],input[type=search],select,textarea{width:100%;font:inherit;color:var(--ink);border:1px solid var(--line);background:#fff;border-radius:8px;padding:9px 12px;font-size:14.5px}input[type=date],input[type=time]{padding-right:10px}input[type=date]::-webkit-calendar-picker-indicator{opacity:.55;cursor:pointer;margin-left:4px;padding:2px}input[type=time]::-webkit-calendar-picker-indicator{opacity:.55;cursor:pointer;margin-left:4px;padding:2px}input[type=date]:hover::-webkit-calendar-picker-indicator{opacity:.9}input[type=time]:hover::-webkit-calendar-picker-indicator{opacity:.9}input::placeholder,textarea::placeholder{color:#94a1b0}input:focus,select:focus,textarea:focus{outline-offset:1px;border-color:var(--brand-2);outline:2px solid #1b548852}select{appearance:none;text-overflow:ellipsis;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%235b6774' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-position:right 12px center;background-repeat:no-repeat;background-size:12px 8px;padding-right:34px}select:-moz-focusring{color:#0000;text-shadow:0 0 0 var(--ink)}select::-ms-expand{display:none}.hint{color:var(--muted);margin-top:5px;font-size:12px}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--bad);background-color:snow}.err{color:var(--bad);margin-top:5px;font-size:12.5px;font-weight:550}fieldset{border:0;margin:0 0 10px;padding:0}legend{text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin-bottom:12px;padding:0;font-size:11px;font-weight:700}.form-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:0 18px;display:grid}.form-grid .full{grid-column:1/-1}.form-actions{border-top:1px solid var(--line-soft);flex-wrap:wrap;align-items:center;gap:10px;margin-top:22px;padding-top:18px;display:flex}.checkline{align-items:flex-start;gap:9px;margin-bottom:12px;display:flex}.checkline input[type=checkbox]{width:16px;height:16px;accent-color:var(--brand-2);flex:none;margin-top:2px}.checkline label{color:var(--ink);margin:0;font-weight:550}.checkline .hint{margin-top:2px}.btn{cursor:pointer;border-radius:var(--radius-lg);border-style:var(--tw-border-style);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);white-space:nowrap;background:var(--brand);border-width:1px;border-color:var(--brand);color:#fff;padding:9px 16px;text-decoration-line:none;transition:background-color .12s ease-out,border-color .12s ease-out,transform 90ms cubic-bezier(.2,.8,.2,1);display:inline-block}.btn:hover{background:var(--brand-2);border-color:var(--brand-2)}.btn:focus-visible{outline:2px solid var(--brand-2);outline-offset:2px}.btn.secondary{color:var(--ink);border-color:var(--line);background:#fff}.btn.secondary:hover{background:#f2f5f8;border-color:#c8d2dd}.btn.danger{background:var(--bad);border-color:var(--bad)}.btn.small{padding:6px 11px;font-size:13px}.btn:disabled{opacity:.55;cursor:not-allowed}.toolbar{flex-wrap:wrap;align-items:flex-end;gap:10px;margin-bottom:16px;display:flex}.toolbar .grow{flex:260px}.toolbar .field{margin-bottom:0}.tabs{border-bottom:1px solid var(--line);flex-wrap:wrap;gap:2px;margin-bottom:20px;display:flex}.tabs a{color:var(--ink-2);border-bottom:2px solid #0000;margin-bottom:-1px;padding:9px 14px;font-size:14px;font-weight:550;text-decoration:none}.tabs a:hover{color:var(--ink)}.tabs a[aria-current=page]{color:var(--brand-2);border-bottom-color:var(--brand-2);font-weight:650}.pager{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-top:16px;display:flex}.pager .count{color:var(--muted);font-size:13px}.defs{grid-template-columns:max-content minmax(0,1fr);gap:10px 20px;margin:0;font-size:14px;display:grid}.defs dt{color:var(--muted);text-transform:uppercase;letter-spacing:.4px;padding-top:2px;font-size:11px;font-weight:700}.defs dd{min-width:0;margin:0}.prose{max-width:68ch;color:var(--ink-2);font-size:14px}.prose p{margin:0 0 12px;line-height:1.6}.prose p:last-child{margin-bottom:0}.rec-head{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.rec-no{color:var(--ink-2);background:var(--brand-tint);border-radius:6px;padding:3px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px}.result-row{border-bottom:1px solid var(--line-soft);padding:12px 0}.result-row:last-child{border-bottom:0}.result-row .rname{font-weight:600}.result-row .rmeta{color:var(--muted);margin-top:2px;font-size:13px}.flash{border:1px solid #0000;border-radius:8px;margin-bottom:20px;padding:12px 16px;font-size:14px}.flash-ok{background:var(--ok-bg);color:var(--ok);border-color:var(--ok-line)}.flash-warn{background:var(--warn-bg);color:var(--warn);border-color:var(--warn-line)}.flash-bad{background:var(--bad-bg);color:var(--bad);border-color:var(--bad-line)}.banner{border:1px solid #0000;border-radius:8px;gap:12px;margin-bottom:16px;padding:14px 16px;font-size:14px;display:flex}.banner.warn{background:var(--warn-bg);border-color:var(--warn-line);color:var(--warn)}.banner.bad{background:var(--bad-bg);border-color:var(--bad-line);color:var(--bad)}.banner.ok{background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok)}.banner strong{margin-bottom:2px;display:block}.banner ul{margin:6px 0 0;padding-left:18px}.empty{text-align:center;color:var(--ink-2);padding:34px 20px}.empty p{margin:0 0 6px}body.auth{background:#0d2941 url(login-bg.jpg) 50%/cover no-repeat;min-height:100dvh}.auth-page{max-width:420px;margin:0 auto;padding:56px 20px 64px}.auth-card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:28px}.auth-brand{text-align:center;margin-bottom:24px}.auth-brand .brand-mark{background:var(--brand);color:#fff;width:44px;height:44px;margin:0 auto 12px;font-size:15px}.auth-brand h1{color:#fff;margin:0 0 4px;font-size:20px}.auth-brand p{color:#ffffffc7;margin:0;font-size:14px}body.auth .auth-page>p.muted{color:#ffffffb8}.site-foot{border-top:1px solid var(--line);background:var(--card);color:var(--muted);padding:20px 28px 34px;font-size:13px}.site-foot p{margin:0 0 4px}@media (max-width:1040px){.grid-3{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width:900px){.shell{grid-template-columns:1fr}.side{flex-flow:wrap;align-items:center;gap:10px;padding:10px 14px}.brand{flex:none}.brand-text,.sidefoot{display:none}.navdrawer-toggle{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.navdrawer-summary{cursor:pointer;-webkit-user-select:none;user-select:none;color:#fff;border-radius:var(--radius-s);background:#ffffff0f;flex:100%;order:2;justify-content:space-between;align-items:center;min-height:44px;padding:10px 12px;font-size:14px;font-weight:600;display:flex}.navdrawer-toggle:focus-visible~.navdrawer-summary{outline-offset:-2px;outline:2px solid #fff}.navdrawer-chev{border-bottom:2px solid #ffffffa6;border-right:2px solid #ffffffa6;flex:none;width:8px;height:8px;transition:transform .15s;transform:rotate(45deg)}.navdrawer-toggle:checked~.navdrawer-summary .navdrawer-chev{transform:rotate(-135deg)}.sidenav{border-radius:var(--radius-s);background:#ffffff0a;flex:100%;order:3;margin-top:8px;padding:6px 4px;display:none}.navdrawer-toggle:checked~.sidenav,.navgroup{display:block}.sidenav ul{margin:0 0 10px;display:block}.sidenav a{white-space:normal;padding:10px 12px;font-size:14.5px}.sidenav a[aria-current=page]{box-shadow:inset 3px 0 #ffffffa6}.topbar{padding:12px 16px}.page{padding:20px 16px 48px}.site-foot{padding:18px 16px 32px}.grid-3,.grid-2{grid-template-columns:minmax(0,1fr)}.who-name{display:none}}@media (max-width:640px){.form-grid{grid-template-columns:1fr}.defs{grid-template-columns:minmax(0,1fr);gap:2px 0}.defs dd{margin-bottom:12px}.page-head h1{font-size:20px}.card{padding:16px}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}.btn:active{transform:none}}@media print{.side,.topbar,.site-foot,.page-actions,.toolbar{display:none}.shell{display:block}body{background:#fff}.card{box-shadow:none;page-break-inside:avoid;border-color:#ccc}.shell,.side,.main{height:auto;max-height:none;overflow:visible}}:root{--s1:4px;--s2:8px;--s3:12px;--s4:16px;--s5:24px;--s6:32px;--s7:48px;--s8:64px}h1,.page-head h1{letter-spacing:-.021em;font-optical-sizing:auto;font-size:clamp(20px,2.2vw,24px);line-height:1.2}h2,.card-head h2{letter-spacing:-.012em;line-height:1.3}.stat .n{letter-spacing:-.028em;font-optical-sizing:auto;line-height:1.08}th,legend,.navgroup,.defs dt{letter-spacing:.055em}p,td,dd,.hint{letter-spacing:0}.hint,.rmeta,.pager .count{line-height:1.5}.rec-no,td.num,.num,code{font-variant-numeric:tabular-nums}@media (pointer:coarse){.btn,.btn.small{justify-content:center;align-items:center;min-height:44px;display:inline-flex}input[type=text],input[type=email],input[type=password],input[type=date],input[type=time],input[type=tel],input[type=url],input[type=number],input[type=search],select,textarea{min-height:44px;font-size:16px}.sidenav a{align-items:center;min-height:44px;display:flex}.checkline input[type=checkbox]{width:20px;height:20px}.signout{align-items:center;min-height:44px;display:inline-flex}td a,.defs a{padding:2px 0;display:inline-block}}@media (max-width:640px){input,select,textarea{font-size:16px}}a,button,.btn,input[type=submit],label,summary{touch-action:manipulation}a:focus-visible,button:focus-visible,.btn:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--brand-2);outline-offset:2px;border-radius:4px}.side a:focus-visible{outline-color:#fff}.btn{transition:background-color .12s ease-out,border-color .12s ease-out,transform 90ms ease-out}.btn:active{transform:translateY(1px)scale(.995)}tbody tr{transition:background-color .11s ease-out}.sidenav a,.tabs a,td a{transition:background-color .12s ease-out,color .12s ease-out}.skip{z-index:10;background:var(--brand);color:#fff;border-radius:0 0 8px;padding:10px 16px;font-weight:650;text-decoration:none;position:absolute;top:0;left:-9999px}.skip:focus{left:0}.btn[aria-busy=true]{opacity:.7;pointer-events:none}.btn[aria-busy=true]:after{content:"";vertical-align:-1px;border:2px solid;border-right-color:#0000;border-radius:50%;width:12px;height:12px;margin-left:8px;animation:.6s linear infinite ls-spin;display:inline-block}@keyframes ls-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.btn[aria-busy=true]:after{opacity:.5;animation:none}}.form-actions .btn.danger{margin-left:auto}label .req{font-weight:700}label .req:after{content:" required";position:absolute;left:-9999px}@media (prefers-contrast:more){:root{--line:#9aa7b5;--muted:#4d5966}.card{border-width:1.5px}.pill{outline:1px solid}}@media (max-width:900px){.btn,.btn.small,button,input[type=submit]{justify-content:center;align-items:center;min-height:44px;display:inline-flex}.sidenav a{align-items:center;min-height:44px;display:flex}input[type=text],input[type=email],input[type=password],input[type=date],input[type=time],input[type=number],input[type=search],select,textarea,.gsearch input{min-height:44px}.checkline input[type=checkbox]{width:22px;height:22px}.signout{align-items:center;min-height:44px;display:inline-flex}td .btn.small{width:100%}}.acct{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 20px;position:relative;overflow:hidden}.acct:before{content:"";width:3px;position:absolute;inset:0 auto 0 0}.acct.client:before{background:var(--brand-2)}.acct.office:before{background:var(--accent,#8a6d29)}.acct .kind{letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:0 0 8px;font-size:11px;font-weight:700}.acct .bal{letter-spacing:-.028em;font-variant-numeric:tabular-nums;font-size:26px;font-weight:650;line-height:1.1}.acct .meta{color:var(--muted);margin-top:6px;font-size:13px}.acct .meta strong{color:var(--ink-2);font-weight:650}.check{border-radius:var(--radius);border:1px solid #0000;align-items:flex-start;gap:14px;margin-bottom:16px;padding:16px 18px;font-size:14px;display:flex}.check.pass{background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok)}.check.fail{background:var(--bad-bg);border-color:var(--bad-line);color:var(--bad)}.check .mark{background:currentColor;border-radius:50%;flex:none;place-items:center;width:22px;height:22px;margin-top:1px;font-size:13px;font-weight:800;display:grid}.check .mark span{color:#fff}.check.fail .mark span{color:var(--bad-bg)}.check.pass .mark span{color:var(--ok-bg)}.check strong{margin-bottom:3px;display:block}.check .figures{font-variant-numeric:tabular-nums;margin-top:8px}.check .figures span{margin-right:18px;display:inline-block}.ledger td,.ledger th{padding:9px 12px}.ledger .money{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}.ledger .in{color:var(--ok);font-weight:600}.ledger .out{color:var(--bad);font-weight:600}.ledger .running{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;font-weight:650}.ledger tbody tr.reversed td{opacity:.65;text-decoration:line-through;background:var(--row-correction)}.ledger tbody tr.reversal td{background:var(--row-correction)}.ledger .ref{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.dirtag{letter-spacing:.05em;text-transform:uppercase;font-size:10.5px;font-weight:700}.segmented{border:1px solid var(--line);border-radius:9px;gap:0;margin-bottom:18px;display:flex;overflow:hidden}.segmented input{opacity:0;pointer-events:none;position:absolute}.segmented label{text-align:center;cursor:pointer;color:var(--ink-2);border-right:1px solid var(--line);background:#fff;flex:1;margin:0;padding:12px 14px;font-size:14px;font-weight:600;transition:background .12s ease-out,color .12s ease-out}.segmented label:last-of-type{border-right:0}.segmented label:hover{background:#f2f5f8}.segmented input:checked+label{background:var(--brand);color:#fff}.segmented input:focus-visible+label{outline:2px solid var(--brand-2);outline-offset:-2px}.segmented label small{opacity:.75;margin-top:2px;font-size:11.5px;font-weight:500;display:block}.avail{background:var(--brand-tint);border:1px solid #cfdcea;border-radius:9px;margin-bottom:16px;padding:12px 14px;font-size:14px}.avail .amt{letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:20px;font-weight:650}.avail .lbl{color:var(--ink-2);font-size:12.5px}.irreversible{background:var(--warn-bg);border:1px solid var(--warn-line);color:var(--warn);border-radius:8px;align-items:flex-start;gap:10px;margin:18px 0;padding:12px 14px;font-size:13.5px;display:flex}.amount-field input{letter-spacing:-.01em;font-variant-numeric:tabular-nums;padding:12px 14px;font-size:22px;font-weight:600}.amount-field .prefix{color:var(--muted);font-weight:600}@media print{.ledger{font-size:11pt}.ledger tbody tr:hover{background:0 0}.acct{break-inside:avoid}.check,.irreversible{border-width:1pt}.no-print,.side,.topbar,.site-foot{display:none!important}.shell{display:block}.main{width:100%}.shell,.side,.main{height:auto;max-height:none;overflow:visible}}@media (max-width:900px){.segmented{flex-direction:column}.segmented label{border-right:0;border-bottom:1px solid var(--line);text-align:left}.segmented label:last-of-type{border-bottom:0}}input[type=file]{background:var(--card);border:1px solid var(--line);width:100%;font:inherit;color:var(--ink);border-radius:8px;padding:10px 12px}input[type=file]:disabled{opacity:.55;cursor:not-allowed}input[type=file]::file-selector-button{font:inherit;background:var(--line-soft);color:var(--ink);border:1px solid var(--line);cursor:pointer;border-radius:6px;margin-right:12px;padding:7px 12px;font-weight:600}.row-actions{white-space:nowrap}.row-actions>*+*{margin-left:8px}.ls-dialog{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);width:min(640px,100vw - 32px);height:93dvh;max-height:none;color:var(--ink);padding:0;box-shadow:0 12px 40px #141c2838}.ls-dialog[open]{flex-direction:column;display:flex}.ls-dialog:not(.assistant)>form{flex-direction:column;height:100%;min-height:0;margin:0;padding:0;display:flex}.ls-dialog:not(.assistant)>form>h2,.ls-dialog:not(.assistant)>form>.dialog-sub{flex:none;padding-left:24px;padding-right:24px}.ls-dialog:not(.assistant)>form>h2{padding-top:22px}.ls-dialog h2{letter-spacing:-.01em;margin:0;font-size:18px}.ls-dialog .dialog-sub{color:var(--muted);max-width:68ch;margin:4px 0 0;font-size:14px}.ls-dialog .dialog-body{overscroll-behavior:contain;flex:auto;min-width:0;min-height:0;padding:18px 24px 4px;overflow:hidden auto}.ls-dialog:not(.assistant)>form>.dialog-body:first-of-type{padding-top:22px}.ls-dialog .field{margin-bottom:4px}.ls-dialog input[type=text]{width:100%;font:inherit;border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:8px;padding:10px 12px}.dialog-actions{border-top:1px solid var(--line-soft);background:var(--card);border-radius:0 0 var(--radius) var(--radius);flex:none;justify-content:flex-end;align-items:center;gap:10px;margin-top:0;padding:16px 24px;display:flex}body:has(dialog[open]){overflow:hidden}tr.is-removed>td{opacity:.68}tr.is-removed>td:first-child>a{text-decoration:line-through}@media (max-width:900px){input[type=file]{min-height:44px}.row-actions{white-space:normal}.row-actions>*+*{margin-top:8px;margin-left:0}.row-actions .btn.small{width:100%}.ls-dialog input[type=text]{min-height:44px}.dialog-actions{flex-direction:column-reverse;align-items:stretch}.dialog-actions .btn{width:100%}}.report-group{margin-top:20px}.report-group:first-child{margin-top:0}.report-group+.report-group{border-top:1px solid var(--line-soft);padding-top:18px}.report-group-h{align-items:baseline;gap:8px;margin:0 0 8px;font-size:14.5px;font-weight:650;display:flex}.ai-scan-card{background:var(--brand-tint);border-color:var(--line);justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px;display:flex}.ai-scan-card[hidden]{display:none}.ai-scan-card p{color:var(--muted);max-width:52ch;margin:4px 0 0;font-size:13.5px}.ai-scan-status{min-height:1.2em;margin:10px 0;font-size:13.5px}.ai-scan-status.ok{color:var(--ok)}.ai-scan-status.bad{color:var(--bad)}.scan-go{display:grid;gap:10px;min-width:260px}.scan-go input[type=file]{min-width:0}.ai-suggested{box-shadow:0 0 0 1px var(--brand-2);background:var(--brand-tint)!important;border-color:var(--brand-2)!important}.pill.ai-tag{background:var(--brand-tint);color:var(--brand-2);border:1px solid var(--brand-2)}.ai-summarise-row{flex-wrap:wrap;align-items:center;gap:12px;margin:-6px 0 16px;display:flex}.field:has(.ai-suggested):after{content:"AI suggested, review before saving";color:var(--brand-2);letter-spacing:.01em;margin-top:4px;font-size:11.5px;font-weight:600;display:block}@media (max-width:640px){.ai-scan-card{flex-direction:column;align-items:flex-start}}.transcript{margin:0 0 20px;padding:0;list-style:none}.transcript li{border-left:3px solid var(--line);margin-bottom:14px;padding:2px 0 2px 14px}.transcript .tr-who{color:var(--ink-2);letter-spacing:.01em;margin:0 0 2px;font-size:12.5px;font-weight:600}.transcript .tr-who .muted{margin-left:8px;font-weight:400}.transcript .tr-body{white-space:pre-wrap;margin:0}.transcript li.tr-client{border-left-color:var(--brand-2)}.transcript li.tr-staff{border-left-color:var(--line);margin-left:20px}.transcript li.tr-ai{border-left-color:var(--warn-line);background:var(--warn-bg);border-radius:0 var(--radius-s) var(--radius-s) 0;margin-left:20px;padding:8px 12px 8px 14px}.transcript li.tr-ai .tr-who{color:var(--warn)}.say-form{border-top:1px solid var(--line-soft);padding-top:16px}.intake-checklist{margin-top:4px}.intake-checklist summary{cursor:pointer;color:var(--brand-2);padding:6px 0;font-size:13.5px;font-weight:600}.intake-checklist ul{color:var(--ink-2);margin:4px 0 0;padding-left:20px;font-size:14px}.intake-checklist li{margin-bottom:4px}.ls-dialog.wide{width:min(920px,100vw - 32px)}.ls-dialog h3{text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:22px 0 12px;font-size:14px}.ls-dialog h3:first-of-type{margin-top:8px}@media (max-width:640px){.transcript li.tr-staff,.transcript li.tr-ai{margin-left:0}}@media print{.transcript li{break-inside:avoid}.intake-checklist,.ai-summarise-row,.say-form{display:none}}.ls-dialog.assistant{width:min(880px,100vw - 24px);padding:0}.ls-dialog.assistant[open]{flex-direction:column;display:flex}.asst-head{border-bottom:1px solid var(--line-soft);justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 22px 14px;display:flex}.asst-head .dialog-sub{max-width:56ch;margin:4px 0 0}.asst-log{overscroll-behavior:contain;background:var(--bg);flex-direction:column;flex:auto;gap:14px;min-height:180px;padding:18px 22px;display:flex;overflow-y:auto}.asst-empty p{color:var(--ink-2);margin:0 0 10px;font-size:14px}.asst-empty ul{flex-wrap:wrap;gap:8px;margin:0 0 12px;padding:0;list-style:none;display:flex}.asst-eg{font:inherit;cursor:pointer;text-align:left;background:var(--card);color:var(--brand-2);border:1px solid var(--line);border-radius:999px;padding:7px 14px;font-size:13.5px}.asst-eg:hover{border-color:var(--brand-2);background:var(--brand-tint)}.asst-msg{max-width:88%}.asst-msg .asst-body{white-space:pre-wrap;margin:0;font-size:14.5px}.asst-note{color:var(--muted);letter-spacing:.01em;margin:0 0 4px;font-size:12px;font-weight:600}.asst-user{text-align:left;background:var(--brand);color:#fff;border-radius:var(--radius) var(--radius) 2px var(--radius);align-self:flex-end;padding:11px 14px}.asst-user .asst-note{color:#ffffffbf}.asst-assistant{background:var(--card);border:1px solid var(--line);border-radius:var(--radius) var(--radius) var(--radius) 2px;align-self:flex-start;padding:11px 14px}.asst-error{background:var(--bad-bg);border:1px solid var(--bad-line);color:var(--bad);border-radius:var(--radius);align-self:flex-start;padding:11px 14px}.asst-form{border-top:1px solid var(--line-soft);background:var(--card);padding:14px 22px 16px}.composer{border:1px solid var(--line);background:var(--card);border-radius:14px;padding:6px;transition:border-color .12s,box-shadow .12s}.composer:focus-within{border-color:var(--brand-2);box-shadow:0 0 0 3px #1b548824}.composer-row{align-items:flex-end;gap:6px;display:flex}.composer-attach{cursor:pointer;width:36px;height:36px;color:var(--ink-2);border-radius:999px;flex:none;place-items:center;font-size:16px;line-height:1;display:grid}.composer-attach:hover{background:var(--line-soft);color:var(--ink)}.composer-attach:focus-within{outline:2px solid var(--brand-2);outline-offset:-2px}.asst-form textarea{min-width:0;color:var(--ink);font:inherit;resize:none;background:0 0;border:0;outline:none;flex:1;max-height:200px;margin:0;padding:8px 2px;font-size:14.5px;line-height:1.5;overflow-y:auto}.composer-send{cursor:pointer;background:var(--brand);color:#fff;border:0;border-radius:999px;flex:none;place-items:center;width:36px;height:36px;font-size:16px;line-height:1;display:grid}.composer-send:hover:not(:disabled){background:var(--brand-2)}.composer-send:disabled{background:var(--line);color:var(--muted);cursor:default}.asst-file{color:var(--ink-2);background:var(--brand-tint);border:1px solid var(--line);border-radius:999px;align-items:center;gap:8px;max-width:100%;margin:2px 4px 6px;padding:5px 6px 5px 12px;font-size:13px;display:flex}.asst-file[hidden]{display:none}.asst-file #asst_file_name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.asst-file-icon{flex:none}.asst-file-x{cursor:pointer;color:var(--muted);background:0 0;border:0;border-radius:999px;flex:none;width:22px;height:22px;font-size:17px;line-height:1}.asst-file-x:hover{background:var(--line);color:var(--ink)}.asst-status{color:var(--brand-2);font-weight:600}.asst-status:not(:empty):after{content:" · ";color:var(--muted);font-weight:400}.asst-foot{margin:10px 0 0;font-size:12px}@media (max-width:640px){.ls-dialog.assistant{width:calc(100vw - 12px)}.asst-msg{max-width:100%}.composer-attach,.composer-send{width:44px;height:44px}}@media print{.ls-dialog.assistant,.askbtn{display:none!important}}.tick{letter-spacing:-3px;margin-right:6px;font-size:13px}.tick-one{color:var(--muted)}.tick-two{color:var(--brand-2);font-weight:700}.tick-warn{color:var(--bad);letter-spacing:0}.sup-item .sup-q{white-space:pre-wrap;background:var(--brand-tint);border-radius:var(--radius-s);margin:0 0 10px;padding:11px 14px}.sup-item .sup-a{margin-bottom:16px}.sup-item .sup-label{color:var(--muted);margin:0 0 4px;font-size:12.5px;font-weight:600}.sup-item .sup-body{white-space:pre-wrap;border:1px solid var(--line);border-radius:var(--radius-s);margin:0;padding:11px 14px}.asst-state{color:var(--muted);margin:6px 0 0;font-size:12px}.asst-review{background:var(--bad-bg);border:1px solid var(--bad-line);color:var(--bad);border-radius:var(--radius-s);margin:8px 0 0;padding:9px 12px;font-size:13.5px}.asst-review.ok{background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok)}input.visually-hidden,select.visually-hidden,textarea.visually-hidden{border:0;width:1px;height:1px;min-height:0;margin:0;padding:0}.subnav{border-bottom:1px solid var(--line);flex-wrap:wrap;gap:4px;margin:-8px 0 18px;padding-bottom:2px;display:flex}.subnav a{border-radius:var(--radius-s) var(--radius-s) 0 0;color:var(--ink-2);border:1px solid #0000;border-bottom:none;margin-bottom:-1px;padding:8px 14px;font-size:14px;font-weight:500;text-decoration:none}.subnav a:hover{background:var(--line-soft);color:var(--ink)}.subnav a[aria-current=page]{background:var(--card);color:var(--brand);border-color:var(--line);border-bottom:1px solid var(--card);font-weight:650}.subnav a:focus-visible{outline:2px solid var(--brand-2);outline-offset:-2px}.page:has(>.page-aside){grid-template-columns:minmax(0,1fr) 300px;align-items:start;gap:0 22px;display:grid}.page:has(>.page-aside)>*{grid-column:1}.page:has(>.page-aside)>.page-aside{grid-area:2/2/9999}.page-aside{flex-direction:column;gap:12px;display:flex}.alert{border:1px solid var(--line);border-radius:var(--radius-s);background:var(--card);border-left-width:4px;padding:12px 14px;font-size:13.5px}.alert-title{margin:0 0 4px;font-size:13.5px;font-weight:650}.alert-body{color:var(--ink-2);margin:0;line-height:1.5}.alert-act{margin:8px 0 0}.alert-act a{color:var(--brand-2);font-weight:600}.alert-bad{border-left-color:var(--bad);background:var(--bad-bg)}.alert-bad .alert-title{color:var(--bad)}.alert-warn{border-left-color:var(--warn);background:var(--warn-bg)}.alert-warn .alert-title{color:var(--warn)}.alert-ok{border-left-color:var(--ok);background:var(--ok-bg)}.alert-ok .alert-title{color:var(--ok)}@media (max-width:1100px){.page:has(>.page-aside){display:block}.page:has(>.page-aside)>.page-aside{margin-bottom:18px}}@media print{.page-aside,.subnav{display:none!important}}}@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.block{display:block}.contents{display:contents}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.grow{flex-grow:1}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}:root{--ink:#141c28;--ink-2:#3a4757;--muted:#5b6774;--bg:#f4f6f8;--card:#fff;--line:#dde3ea;--line-soft:#eef1f5;--brand:#12395f;--brand-2:#1b5488;--brand-ink:#0d2941;--brand-tint:#eaf0f6;--ok:#1b6b45;--ok-bg:#e6f4ec;--ok-line:#c4e3d2;--warn:#8a5710;--warn-bg:#fdf2e0;--warn-line:#efd9b3;--bad:#983030;--bad-bg:#fbeceb;--bad-line:#f0cbc9;--radius:10px;--radius-s:7px;--shadow:0 1px 2px #141c280d, 0 3px 12px #141c280b;--side-w:236px}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}/* Export and print actions. The icon sits on the text baseline inside the
   button rather than floating above it, and never shrinks when the label
   wraps on a narrow screen. */
.btn .ico{vertical-align:-3px;flex:none}
.btn:has(.ico){display:inline-flex;align-items:center;gap:6px}
@media print{.btn,.page-actions,[data-print]{display:none!important}}
/* Centre modal dialogs.

   The browser's own stylesheet centres an open modal with `dialog:modal
   { margin: auto }`, but the preflight reset at the top of this file zeroes
   the margin on every element and ::backdrop, which silently beat it. The
   result was that EVERY dialog in LawServa opened pinned to the top left
   corner instead of the middle of the screen. Restoring it on the class is
   enough: a class selector outranks the `*` reset. */
.ls-dialog{margin:auto}

/* ---------------------------------------------------------------------------
   Alert panel: neutral level, and jump-to-section.

   The panel is, and stays, an ordinary grid column in normal flow. It is not
   sticky and not fixed, for the same reason the sidebar is not: nothing in
   this application may ever float over a ledger figure or a diary date.
   Clicking an alert therefore scrolls the record; the panel scrolls away with
   the rest of the page, which is the intended behaviour, not a shortcoming.
   ------------------------------------------------------------------------ */

/* A neutral level for facts that are neither good nor bad, e.g. "Next date".
   Colouring those amber taught people to read amber as "ignore me". */
.alert-info{border-left-color:var(--line);background:var(--card)}
.alert-info .alert-title{color:var(--ink)}

.alert-jump::after{content:" \2193";font-weight:400}

/* Where a jump lands. Without this the heading sits flush against the top edge
   of the scroll container and reads as cut off. */
[data-jump-target]{scroll-margin-top:18px}

/* Arrival feedback. A jump that moves the page but marks nothing leaves the
   reader hunting for what changed, so the target is outlined briefly. This is
   state-transition feedback, not decoration: it answers "where did I land". */
@keyframes ls-landed{
  0%   {box-shadow:0 0 0 3px rgb(27 84 136 / .28)}
  100% {box-shadow:0 0 0 3px rgb(27 84 136 / 0)}
}
.is-landed{border-radius:var(--radius-s);animation:ls-landed 1.6s ease-out 1}
/* The target is focused so screen readers and the keyboard follow the jump
   too. That focus ring is programmatic, not a pointer focus, so it must not
   read as a second highlight on top of the one above. */
[data-jump-target]:focus{outline:none}

@media (prefers-reduced-motion:reduce){
  /* Keep the marker, drop the fade: the outline appears and is removed by
     script instead of animating. Reduced motion means gentler feedback, not
     no feedback. */
  .is-landed{animation:none;box-shadow:0 0 0 3px rgb(27 84 136 / .28)}
}

/* ---------------------------------------------------------------------------
   Phone layout: get to the record faster.

   Measured on a 375x812 handset before this block: the first card of an actual
   matter began 691px down, 85% of the viewport. A fee earner checking a file
   between hearings scrolled almost a full screen past furniture to reach it.
   The furniture was the identity row wrapping onto its own line, and alert
   cards set at desktop density.

   Nothing is hidden that is an ACTION. The signed-in name and the role pill
   are the two things a person already knows about themselves, so they are what
   goes; "Ask LawServa" and "Sign out" both stay, and the firm name is still in
   the menu drawer.
   ------------------------------------------------------------------------ */
@media (max-width:700px){
  .topbar{flex-wrap:nowrap;gap:10px;padding:10px 14px}
  /* Search keeps the row but yields space to the actions beside it. */
  .gsearch{flex:1 1 auto;min-width:0}
  .gsearch input{min-width:0;width:100%}
  .who{flex:none;gap:8px}
  .who-name,.who .pill{display:none}

  /* Alert cards at phone density. They stack ABOVE the record here because a
     phone has no second column, so every line they spend is a line of the file
     pushed off screen. Same words, less padding. */
  .page-aside{gap:8px}
  .alert{padding:10px 12px;font-size:13px}
  .alert-title{font-size:13px}
  .alert-act{margin-top:6px}
}

/* Checkboxes and radios were 22x22, which is under the 24x24 minimum WCAG 2.5.8
   asks for. Two of them in this application set a politically-exposed-person
   flag and mark time billable, so a miss-tap on a phone is an AMLA record or a
   client's bill, not a cosmetic slip. The hidden navdrawer toggle is excluded:
   it is driven by its own 44px label and is never tapped directly. */
input[type=checkbox]:not(.navdrawer-toggle),
input[type=radio]{width:24px;height:24px}
@media (max-width:700px){
  input[type=checkbox]:not(.navdrawer-toggle),
  input[type=radio]{width:26px;height:26px}
}

/* Sidebar group labels ("Practice", "Money", "Firm") were #ffffff6b, which is
   white at 42% and measures 3.81:1 against the navy sidebar. WCAG AA wants
   4.5:1 for text this size, and these are the labels that tell a person which
   part of the system they are looking at, so they are worth reading rather
   than guessing at. Raised to 72% white, measured at 6.0:1. */
.navgroup{color:#ffffffb8}

/* Peek list: label/value pairs in the summary modal. A two-column grid so the
   values line up and the eye can run down them, collapsing to stacked pairs on
   a phone where two columns would leave nothing but wrapped fragments. */
.peek-list{display:grid;grid-template-columns:minmax(0,148px) minmax(0,1fr);gap:8px 16px;margin:0}
.peek-list dt{color:var(--muted);font-size:12.5px;letter-spacing:.02em;text-transform:uppercase;margin:0}
.peek-list dd{color:var(--ink);margin:0;font-size:14px}
.peek-list .peek-bad{color:var(--bad);font-weight:650}
@media (max-width:560px){
  .peek-list{grid-template-columns:minmax(0,1fr);gap:2px 0}
  .peek-list dd{margin-bottom:8px}
}
/* The row is still a link. Only the cursor hints that it summarises first. */

/* ---------------------------------------------------------------------------
   Record overlay.

   A record page opened over the list it was clicked from. The record is loaded
   in an IFRAME rather than injected as HTML: it stays a real document, so its
   own forms, dialogs, alert column and jump-to-section all work natively, no
   two elements can end up sharing an id, and there is no innerHTML anywhere to
   become an injection surface.
   ------------------------------------------------------------------------ */
.ls-dialog.record{width:min(1180px,100vw - 24px);height:94dvh;padding:0}
.ls-dialog.record .rec-frame{flex:auto;width:100%;border:0;display:block;min-height:0;background:var(--bg)}
.ls-dialog.record .dialog-actions{border-top:1px solid var(--line-soft)}
.rec-head-bar{flex:none;display:flex;align-items:center;justify-content:space-between;
  gap:12px;padding:12px 16px;border-bottom:1px solid var(--line-soft);background:var(--card)}
.rec-head-bar p{margin:0;font-size:13px;color:var(--muted);
  min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* The record overlay's header tools: print, download, email.
   ---------------------------------------------------------------------------
   These are UTILITIES. They act on the whole record rather than on anything
   inside it, they are the same three on every record, and they are therefore
   window chrome, not record actions. Putting them here is what keeps the
   footer for the record's own business, and it is why Print is no longer
   repeated at the bottom: one action, one control.

   They borrow the X's exact box - 36px, 1px line, 8px radius, card ground - so
   the header reads as one set of controls rather than two competing styles.

   The gap before the X is deliberate and load bearing. Print puts a client
   file on a shared office printer and Email sends it out of the firm; neither
   may sit flush against the control every reader hits without looking. */
.rec-tools{flex:none;display:flex;align-items:center;gap:6px}
.rec-tool{width:36px;height:36px;flex:none;display:flex;align-items:center;
  justify-content:center;border:1px solid var(--line);border-radius:8px;
  background:var(--card);color:var(--ink-2);cursor:pointer;padding:0}
.rec-tool:hover{background:var(--bg);color:var(--ink)}
.rec-tool:focus-visible{outline:2px solid var(--brand-2);outline-offset:2px}
.rec-tool svg{display:block;pointer-events:none}
.rec-tool[hidden]{display:none}
/* Coarse pointers get the 44px floor, per the density rule: a finger does not
   get a smaller target because a mouse was assumed. */
@media (pointer:coarse){
  .rec-tool{width:44px;height:44px}
}
/* Clear of the X, with room to spare rather than room to just fit. */
.ls-dialog.record .rec-head-bar{padding-right:64px}

/* Inside the overlay the host supplies the chrome, so the embedded document is
   a single column with no sidebar and no fixed viewport height of its own. */
.shell-modal{grid-template-columns:minmax(0,1fr);height:auto;min-height:0;overflow:visible}
.shell-modal .main{height:auto;max-height:none;overflow:visible}
.shell-modal .page{padding-top:18px}

@media print{
  /* Print what is being READ. Without this, printing while the overlay is open
     produces the list underneath and a blank rectangle. */
  body:has(dialog.record[open]) .shell{display:none!important}
  .ls-dialog.record{position:static;width:auto;height:auto;box-shadow:none;border:0}
  .ls-dialog.record .dialog-actions,.rec-head-bar{display:none!important}
}

/* ---------------------------------------------------------------------------
   Modal anatomy, applied to every dialog without exception.

     header  fixed, carries the title and an X in the top RIGHT
     body    the only region that scrolls
     footer  fixed, Close at the BOTTOM LEFT, every action button to its right

   Both close controls are always present together. Two ways out means the exit
   is in reach whether the reader's eye is at the top of a long form or at the
   bottom of it.

   Close is pushed left with margin-right:auto rather than by making the footer
   space-between: a footer with three buttons would spread them across the full
   width, whereas this pins Close left and keeps every action grouped right,
   whatever the count.
   ------------------------------------------------------------------------ */
.dialog-actions [data-dialog-close]{margin-right:auto}

/* The X. position:absolute, NOT fixed or sticky, so the project-wide ban on
   overlay positioning is untouched; and the header reserves room for it below
   so it can never sit on top of a title.

   THE DIALOG ITSELF IS `fixed`, AND THAT IS NOT A CHOICE. 2 Sep 2026.

   This line read `position:relative` for one reason: the X above is absolute
   and wanted a containing block. `fixed` is a containing block for an absolute
   child in exactly the same way, so the X is unaffected - but `relative` was
   ALSO overriding the browser's own `dialog:modal{position:fixed}`, and that
   is what a modal is anchored by. Anchored `relative`/`absolute` instead, an
   open modal is nailed to DOCUMENT coordinates near the top of the page, while
   its ::backdrop keeps covering the viewport. Scroll away from the top and the
   dialog goes with the document: the screen greys out and there is no dialog
   on it. Nothing is clickable, no message appears, and pressing the button
   again does nothing, because the dialog is already open a few thousand pixels
   above.

   WHY IT HID FOR SO LONG, and why three attempts to reproduce it failed. On an
   ordinary page it cannot happen: `.shell` is `height:100dvh; overflow:hidden`,
   so the document never scrolls and document coordinates and viewport
   coordinates are the same thing. It happens only where a document DOES
   scroll, which in this application is exactly one place - inside the record
   overlay's iframe. Measured on TRP/2026/0007: a 5,519px record in a 622px
   frame, carrying 29 dialogs, every one of them past the fold. So it broke on
   every record, for every reader who had scrolled - which is every reader,
   since the panels are most of a screen down - and on nothing else.

   Reported twice (#1 "Record Decision stuck after submission", #8) and both
   screenshots show it exactly: the record dimmed, header and footer normal,
   and no dialog anywhere on the screen.

   This is not the sticky/fixed chrome the house rule bans. A native modal is
   already in the top layer and already viewport-anchored by the platform; this
   restores that default rather than adding overlay chrome of our own. There is
   ONE position declaration for .ls-dialog in this file and it is this one -
   see the height rule at the end for what happens when a second one appears. */
.ls-dialog{position:fixed}
.dialog-x{position:absolute;top:12px;right:12px;z-index:2;width:36px;height:36px;
  display:flex;align-items:center;justify-content:center;
  border:1px solid var(--line);border-radius:8px;background:var(--card);
  color:var(--ink-2);font-size:19px;line-height:1;cursor:pointer;padding:0}
.dialog-x:hover{background:var(--bg);color:var(--ink)}
.dialog-x:focus-visible{outline:2px solid var(--brand-2);outline-offset:2px}
/* Room for it, so a long title wraps before it reaches the X. */
.ls-dialog:not(.assistant)>form>h2,.ls-dialog:not(.assistant)>form>.dialog-sub{padding-right:56px}
.asst-head,.rec-head-bar{padding-right:56px}

@media (max-width:700px){
  /* The footer stacks on a phone, so "left" stops meaning anything. Close goes
     last in the stack, which is furthest from the thumb's primary action. */
  .dialog-actions [data-dialog-close]{margin-right:0}
}

/* The assistant's own form is the flex child that grows, so its new footer
   must not be squeezed by it. Same three-part anatomy as every other modal:
   header, scrolling middle, fixed footer with Close bottom left. */
.ls-dialog.assistant > form{flex:1 1 auto;min-height:0}
.ls-dialog.assistant > .dialog-actions{flex:none}

/* Some dialogs put the h2 directly under <dialog> rather than under a form
   (undertakings carries two forms, so no single one can own the heading).
   Reserve room for the X there too, or it lands on top of the title. */
.ls-dialog:not(.assistant)>h2,.ls-dialog:not(.assistant)>.dialog-sub{padding-right:56px}

/* ---------------------------------------------------------------------------
   MODAL HEADER: it must be tall enough to CONTAIN the X, not just clear it.

   The X is a 36px control inset 12px from the top and the right, so it claims
   the first 60px of the dialog. A title-only header was 44px tall (22px of
   padding plus one 22px line), so the X hung 16px below the header into the
   scrolling body and sat over the first field; on the record overlay it hung
   over the iframe. Reserving room to the SIDE of the title, which is all the
   previous rule did, never fixed that: the shortfall is vertical.

   So the title row itself gets a 60px floor and centres its text in that row.
   The X and the title then share one optical line at every title length, the
   header keeps one height whether or not a subtitle follows, and nothing the
   user reads or clicks can ever end up underneath the X.

   Still layout, not positioning: a min-height and a flex row. The project-wide
   ban on sticky/fixed is untouched and the page's count stays 0.
   ------------------------------------------------------------------------ */
.ls-dialog:not(.assistant)>form>h2,
.ls-dialog:not(.assistant)>h2{
  display:flex;align-items:center;
  flex:none;min-height:60px;
  padding:0 56px 0 24px;
}
/* The subtitle is part of the header, so it carries the same gutters. Stating
   the left padding here as well is what fixes the h2-outside-a-form dialogs
   (admin, undertakings, the STR register), whose title and subtitle were
   flush against the dialog edge because only the in-form rule set it. */
.ls-dialog:not(.assistant)>form>.dialog-sub,
.ls-dialog:not(.assistant)>.dialog-sub{flex:none;padding:0 56px 10px 24px;margin-top:2px}

/* The header is fixed and the body scrolls under it, so the boundary has to be
   drawn or the first row of a long form appears to be part of the title. The
   footer already carries the same hairline. */
.ls-dialog:not(.assistant) .dialog-body{border-top:1px solid var(--line-soft)}

/* A dialog with no header at all: the body starts at the top edge, so it must
   clear the whole 60px the X occupies rather than the 22px it used to.
   `:first-of-type` was the wrong test - it matches the first DIV, which is the
   body on nearly every dialog here, header or no header. `:first-child` is the
   condition actually meant. */
.ls-dialog:not(.assistant)>form>.dialog-body:first-of-type{padding-top:18px}
.ls-dialog:not(.assistant)>form>.dialog-body:first-child,
.ls-dialog:not(.assistant)>.dialog-body:first-child{padding-top:68px;border-top:0}

/* The two head bars that are not a bare h2 need the same floor. Without it the
   record overlay's 42px label strip left the X overhanging the iframe. */
.rec-head-bar,.asst-head{min-height:60px}

@media (max-width:640px){
  /* Narrow screens: the X keeps its 44px touch target's worth of room and the
     gutters tighten, but the 60px floor stays - that is the whole point. */
  .ls-dialog:not(.assistant)>form>h2,
  .ls-dialog:not(.assistant)>h2{padding-left:18px;padding-right:52px}
  .ls-dialog:not(.assistant)>form>.dialog-sub,
  .ls-dialog:not(.assistant)>.dialog-sub{padding-left:18px;padding-right:52px}
  .ls-dialog .dialog-body{padding-left:18px;padding-right:18px}
  .dialog-actions{padding-left:18px;padding-right:18px}
}

/* A form section heading on a PAGE. `.ls-dialog h3` already styles the same
   heading inside a modal; lib/client_form.php and lib/matter_form.php render
   one field set into both shells, so the page shell needs the matching rule or
   the preflight reset leaves the heading indistinguishable from body text. */
form.card > h3{margin:22px 0 12px;font-size:14px;text-transform:uppercase;
  letter-spacing:.06em;color:var(--muted)}
form.card > h3:first-of-type{margin-top:0}

/* The scan card sits INSIDE the client dialog's scrolling body, above the
   fields it fills in, so it must not inherit the page card's outer margin. */
.ls-dialog .ai-scan-card{margin:0 0 18px}

/* A list that was cut short says so, in the same place and the same voice on
   every screen. Reads as a statement about the list, not as a warning: the
   data is not wrong, there is simply more of it. */
.list-capped{margin:14px 0 0;padding-top:12px;border-top:1px solid var(--line-soft);
  font-size:13.5px;color:var(--ink-2)}
.list-capped .muted{color:var(--muted)}

/* The confirm dialog deliberately keeps the standard modal height. The house
   rule is one height for every modal in the application, so the footer buttons
   land in the same place whichever dialog is open; the existing document
   removal dialog is equally short and equally tall. Consistency of position
   beats a snug box. */

/* ===========================================================================
   LAYOUT CORRECTIONS, 4 Aug 2026
   ===========================================================================
   1. A 640px floor on EVERY table put a horizontal scrollbar on summary
      tables far narrower than that. Measured on the reconciliation screen at
      1440px: the container is 517px and the table was forced to 640px, so
      balance per statement, less unpresented cheques, adjusted balance, cash
      book and THE DIFFERENCE - the entire arithmetic the screen exists for -
      sat behind a scrollbar. Same on the diary's Team card and the matter's
      deadline table, which clipped a deadline title mid-word.

      The floor is meant for the six-column list tables, so it now applies
      only to tables that actually have six columns. No markup change: :has()
      counts the header cells, and it is already relied on elsewhere in this
      file.
   ------------------------------------------------------------------------ */
table{min-width:0}
.table-scroll table:has(thead th:nth-child(6)){min-width:720px}

/* 2. Inside a dialog there was more space between a label and its own input
      than between one question and the next (4px), so a hint read as if it
      belonged to the field below it. Proximity has to say what goes with
      what. */
.ls-dialog .field{margin-bottom:16px}
.ls-dialog .field:last-child{margin-bottom:0}

/* 3. On the invoice, the fee subtotal, disbursements and SST rendered at 11px
      - smaller than their own labels - because the totals are <th> and th is
      the 11px uppercase column-header style. This is the one screen a client
      reads the figures off. */
tfoot th,tfoot td{font-size:14px;text-transform:none;letter-spacing:0;color:var(--ink)}
tfoot tr:last-child th,tfoot tr:last-child td{font-size:15px}
tfoot tr:last-child th{border-top:1px solid var(--line)}

/* 4. In the cash book the narrative was the NARROWEST column: auto layout gave
      the width to the fixed-length date and entry-number columns and wrapped
      "Deposit received on sale of No. 12 Jalan Bangsar" over five lines. */
.ledger th:nth-child(1),.ledger td:nth-child(1),
.ledger th:nth-child(2),.ledger td:nth-child(2){width:1%;white-space:nowrap}
.ledger th:nth-child(3),.ledger td:nth-child(3){width:40%}
.ledger .money,.ledger .running{width:1%}

/* 5. One advisory alert took 300px off the working table and doubled the row
      height on the primary screens. The column is narrower now, and it gives
      up and stacks sooner. */
.page:has(>.page-aside){grid-template-columns:minmax(0,1fr) 260px}
@media (max-width:1279px){
  .page:has(>.page-aside){display:block}
  .page:has(>.page-aside)>.page-aside{margin-bottom:18px}
}

/* 6. Placeholder text carries real instruction here ("Name, client number,
      NRIC, SSM number or email"), and it was the single failing contrast pair
      in the palette: measured 2.63:1 against white, where AA needs 4.5. The
      source comment claimed it passed. This is 4.56:1. */
input::placeholder,textarea::placeholder{color:#6b7785}

/* 7. A record reference inside a table cell stretched to a full-width tinted
      bar, reading as a selected row. Everywhere else it is an inline span. */
.rec-no{display:inline-block}

/* 8. On the Risk radar the title, the reason and the actions were three
      paragraphs with no separation at all, at 150 characters a line. */
.report-group>p{max-width:78ch}
.report-group>p+p{margin-top:6px}
.report-group>p:last-child{margin-top:12px}
.report-group+.report-group{margin-top:18px}

/* 9. Explanatory prose under the cards ran the full width of the page. */
.page>p.muted,.page>p.hint{max-width:78ch}

/* 10. --accent was never defined, so every office account carried a gold that
       exists nowhere else in the system. Point it at the warning token, which
       is what an office-account marker actually means here. */
:root{--accent:var(--warn)}

/* 11. On a phone the three counters cost 300px of the first screen to say
       "0, 0, 6", pushing the diary itself more than a screen down. They are
       glanceable numbers, not cards. */
@media (max-width:700px){
  .grid-3:has(>.card.stat){grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
  .grid-3>.card.stat{padding:12px;margin-bottom:0}
  .card.stat .n{font-size:22px}
  .card.stat .l{font-size:12px}
}

/* 12. The support reference on the error page wrapped mid-token, and it is the
       one string a user has to read back down a telephone. */
.err-ref{white-space:nowrap;display:inline-block}

/* ===========================================================================
   LAWSERVA DESIGN SYSTEM v2 - 7 Aug 2026
   ===========================================================================

   Written as one appended layer so it wins on cascade order without editing
   the minified Tailwind build above it. Every value here is a deliberate
   choice, not a default.

   Design read: a dense, daily-use legal operations tool for a KL firm of
   3-15 people. The visual family is instrument-grade (precise, calm,
   confident), not marketing. Density is a FEATURE: a conveyancing clerk with
   40 live files scans for the one row that matters. So the gains here come
   from hierarchy, alignment, surface layering and motion - never from making
   things bigger or louder.

   What did NOT change, on purpose:
   - Body text stays 15px. This is read by partners in their sixties all day.
     "Compact and premium" is bought with weight, colour and spacing here,
     never by shrinking the thing a lawyer has to read.
   - The navy brand. It is measured, verified and already correct.
   - The layout law: no position:fixed, no position:sticky. An open <dialog>
     is exempt because the user agent puts it in the top layer.
   ------------------------------------------------------------------------ */

:root {
  /* --- Surface layering ------------------------------------------------
     Four planes, so depth comes from tone before it comes from shadow.
     bg      the page
     card    a raised surface sitting on the page
     s2      a recessed fill INSIDE a surface (table head, toolbar, well)
     s3      the hover/active state of a row or control on a surface       */
  /* RENAMED 24 August 2026, and the reason is worth stating plainly.
     These two were declared HERE as colours while the spacing scale at the top
     of this stylesheet already declared --s2: 8px and --s3: 12px. The later
     :root wins, so the two middle steps of the firm's spacing scale silently
     became colours, and any author writing `padding: var(--s3)` got an invalid
     value - the declaration is dropped at computed-value time and the padding
     is simply 0, with no warning anywhere. That is not hypothetical: it is how
     .mt-3 and .mt-4 shipped with no margin at all (see the note beside those
     utilities at the end of this file), pushing buttons flush against the
     control above them on three screens.
     Naming them for what they ARE ends the collision. Every use was rewritten
     with them, and since every use already resolved to these colours, nothing
     renders differently - verified by diffing the computed background, colour
     and border of every element on the Diary, Matters, Billing, Client Account
     and Firm Management before and after. */
  --surface-sunk: #f7f9fb;
  --surface-active: #f1f5f9;
  --s-hover: #f6f8fa;

  /* --- Information -----------------------------------------------------
     The semantic set was missing a neutral-informational level, so facts
     that are neither good nor bad were being coloured amber. That taught
     people to read amber as "ignore me", which is the opposite of what a
     warning is for. Measured 6.90:1 on its own background.               */
  --info:      #1b5488;
  --info-bg:   #eaf1f8;
  --info-line: #c6d9ea;

  /* --- A correction on the ledger -------------------------------------
     A CLIENT-MONEY CORRECTION IS TWO ROWS, AND THEY MUST BE FINDABLE AS A
     PAIR IN BOTH THEMES.

     Postings are immutable, so a mistake is corrected by a reversal entry
     posted later. The wrong entry therefore sits where it always sat and
     its correction sits at the bottom of the ledger, often pages apart, and
     the reporting accountant's whole job on that ledger is to find both
     halves. One faint tint, shared by the two rows, is what lets the eye
     do it; the strike-through and dimming on `tr.reversed` say which half
     is which.

     THE DEFECT THIS TOKEN EXISTS TO FIX. The tint was the literal #fffdf7,
     declared once and never redefined for the theme that then existed. It is
     kept as a TOKEN rather than folded back to a literal because a semantic
     name is what made the fault findable in the first place, and because the
     row it colours is a client-account reversal - the one row on the ledger
     that exists to be read.

     The dark theme it was redefined for is gone: LawServa is light only,
     Alex, 24 August 2026. Nothing below needs a second value any more.

     CR-6 governs the dark value: the SEMANTICS are fixed and only the
     lightness moves, so the dark tint is the same low-chroma amber the dark
     palette already carries as --warn-bg rather than a new hue.            */
  --row-correction: #fffdf7;

  /* AND THE ROW THAT WAS CORRECTED WAS ITSELF UNDER AA, IN BOTH THEMES.
     `.ledger tbody tr.reversed td` was opacity .55, and opacity multiplies
     against whatever is behind it, so the text was effectively rgb(127,129,133)
     on the tint: 3.87:1, in its resting state, which is the only state it has.
     That is the same defect and very nearly the same number as the .row-actions
     one written up further down this file (3.86:1), on a row that records
     somebody's client money going out of a client account and coming back.
     Raised to .65, measured at 5.30:1 light and 6.46:1 dark, which still reads
     as visibly de-weighted beside a neighbouring row at full strength. The
     strike-through, not the dimming, is what says the entry was corrected. */

  /* --- Radius hierarchy ------------------------------------------------
     Six radii were in use with no rule (7, 8, 9, 10, 14, 999). Four now,
     each with a job. Nesting always steps DOWN, never up.                */
  --r-xs:   6px;   /* a chip or a control nested inside another control */
  --r-sm:   8px;   /* controls: button, input, select, row hover        */
  --r-md:   12px;  /* surfaces: card, dialog, popover, panel            */
  --r-full: 999px; /* status badges and avatars ONLY                    */

  /* --- Elevation -------------------------------------------------------
     Tinted to the ink hue, never pure black: a black shadow on a cool
     grey page reads as dirt. Four steps, and step 0 is the common case -
     in a dense tool a hairline separates better than a shadow.          */
  --e-0: none;
  --e-1: 0 1px 2px rgb(20 28 40 / .05);
  --e-2: 0 1px 2px rgb(20 28 40 / .06), 0 8px 20px -8px rgb(20 28 40 / .14);
  --e-3: 0 2px 6px rgb(20 28 40 / .08), 0 24px 48px -12px rgb(20 28 40 / .24);

  /* --- Motion ----------------------------------------------------------
     One easing curve for entrances and one for exits, mirrored, so a
     panel returns along the path it arrived by. Durations are short: this
     is a tool people use for eight hours, not a demo.                    */
  --ease-out: cubic-bezier(.2, .8, .2, 1);
  --ease-in:  cubic-bezier(.8, .2, 1, .2);
  --t-fast:   110ms;
  --t-base:   170ms;
  --t-slow:   240ms;

  /* Focus is one ring everywhere, so it is never mistaken for a state. */
  --focus: 0 0 0 3px rgb(27 84 136 / .28);
}

/* ===========================================================================
   1. TYPOGRAPHY
   ===========================================================================
   Tracking is size-specific: large text reads too loose as it grows and
   wants negative tracking; small caps labels want positive. A single
   letter-spacing value is wrong somewhere by definition.
   ------------------------------------------------------------------------ */

.page-head h1 { font-size: clamp(21px, 2vw, 24px); font-weight: 640; letter-spacing: -.021em; line-height: 1.18; }
.page-head .sub { font-size: 13.5px; line-height: 1.5; }

/* Section headings sit BELOW the page title in weight, not just in size.
   They were 16.5px/650, close enough to h1 to compete with it. */
h2, .card-head h2 { font-size: 15px; font-weight: 650; letter-spacing: -.006em; line-height: 1.35; }

/* An eyebrow above a group. Used for the small caps labels already in the
   system (legend, .defs dt, th) so they share one voice. */
th, legend, .navgroup, .defs dt, .acct .kind, .ls-dialog h3 {
  font-size: 11px; font-weight: 700; letter-spacing: .055em; text-transform: uppercase;
}

/* Numbers are always tabular and always tracked in, so a column of ringgit
   figures forms a straight edge the eye can run down. */
.metric-n, .stat .n, .acct .bal, td.num, .money, .running, .rec-no, code {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ===========================================================================
   2. BUTTONS
   ===========================================================================
   Four intents (primary, secondary, ghost, danger) and two sizes. Ghost was
   missing, so every tertiary action had to borrow the secondary treatment
   and three equally-weighted buttons ended up side by side with nothing
   saying which one was the point of the screen.
   ------------------------------------------------------------------------ */

.btn {
  border-radius: var(--r-sm);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -.002em;
  padding: 8px 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  transition: background-color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              transform 90ms var(--ease-out);
}

/* Feedback lives on the press, not on release: waiting for the click event
   to show anything is what makes an interface feel dead. */
.btn:active:not(:disabled) { transform: scale(.978); }

.btn:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }

.btn.secondary { background: var(--card); border-color: #cfd8e2; color: var(--ink); }
.btn.secondary:hover { background: var(--surface-active); border-color: #b9c6d4; }

/* Ghost: a real action, but not one competing for the eye. */
.btn.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.btn.ghost:hover { background: var(--surface-active); color: var(--ink); }

.btn.small { padding: 6px 11px; font-size: 12.5px; gap: 5px; }

/* An icon-only control still needs a name for a screen reader; the label is
   carried by aria-label, and the box stays square and tappable. */
.btn.icon { padding: 0; width: 34px; height: 34px; flex: none; }
.btn.icon.small { width: 28px; height: 28px; }

.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ===========================================================================
   3. STATUS BADGES
   ===========================================================================
   A pill means STATE. It was also being used for categories, counts and
   labels, so the eye stopped reading colour as meaning. Categories now use
   .tag, which is quiet and square-ish, leaving the pill to say status.
   ------------------------------------------------------------------------ */

.pill {
  border-radius: var(--r-full);
  padding: 2px 9px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .015em;
  line-height: 1.55;
  border: 1px solid transparent;
}
.pill.ok   { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-line); }
.pill.warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.pill.bad  { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }
.pill.info { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }

/* A category, not a state. No colour, no border, reads as a label. */
.tag {
  display: inline-block;
  border-radius: var(--r-xs);
  padding: 2px 7px;
  background: var(--surface-active);
  color: var(--ink-2);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
}

/* ===========================================================================
   4. SURFACES
   ===========================================================================
   A card is for a group that genuinely needs separating from the page. In a
   dense tool most groups do not, so the default is now a hairline surface
   with no shadow, and elevation is reserved for things that actually float.
   ------------------------------------------------------------------------ */

.card {
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--card);
  box-shadow: var(--e-1);
  padding: 18px 20px;
  margin-bottom: 16px;
}

/* A card whose only content is a table should not pad the table away from
   its own border; the table's own cell padding is the padding. */
.card.flush { padding: 0; overflow: hidden; }
.card.flush > .card-head { padding: 14px 18px 12px; margin-bottom: 0; border-bottom: 1px solid var(--line-soft); }
.card.flush > .table-scroll > table { margin: 0; }
.card.flush > .empty { padding: 34px 20px; }
.card.flush > .list-capped { margin: 0; padding: 10px 18px; border-top: 1px solid var(--line-soft); }

.card-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
}
.card-head h2 { margin: 0; }
/* The count beside a section title. Says how much is here before you scan. */
.card-head .count {
  font-size: 12px; font-weight: 600; color: var(--muted);
  background: var(--surface-active); border-radius: var(--r-full); padding: 1px 8px;
}

/* ===========================================================================
   5. DATA TABLES
   ===========================================================================
   The most important surface in the product. Three real problems fixed:

   (a) The header had no fill and the same hairline as every row, so the
       column names floated and a long table lost its heading the moment it
       scrolled. It now sits on a recessed plane (measured 5.59:1 for the
       label colour on that fill, AA for 11px bold).
   (b) The record link in a row was 550 weight in brand-2 with no underline
       offset, so on a dense row it did not read as the way in.
   (c) Below 900px a six-column table was forced to 720px inside a sideways
       scroller. On a phone the Matters list became a spreadsheet you had to
       drag. See section 5b: it becomes a list of records instead.

   Cell alignment stays vertical-align:top, unchanged. A first cell here
   routinely stacks a title, a record number and a status pill, and top is
   the only alignment that keeps those rows readable when they wrap.
   ------------------------------------------------------------------------ */

table { font-size: 13.5px; }

thead th {
  background: var(--surface-sunk);
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  padding: 9px 12px;
  vertical-align: middle;
  white-space: nowrap;
}
thead th:first-child { border-top-left-radius: var(--r-xs); }
thead th:last-child  { border-top-right-radius: var(--r-xs); }

tbody td {
  padding: 10px 12px;
  vertical-align: top;
  border-bottom: 1px solid var(--line-soft);
}
/* A cell that stacks a title over meta needs its own rhythm, not the
   paragraph margins it was inheriting. */
tbody td > div { margin-top: 3px; line-height: 1.4; }
tbody td > div:first-child { margin-top: 0; }

tbody tr { transition: background-color var(--t-fast) var(--ease-out); }
tbody tr:hover { background: var(--s-hover); }
/* Keyboard parity: tabbing to the link in a row highlights the row too, so
   the keyboard user sees what the mouse user sees. */
tbody tr:focus-within { background: var(--brand-tint); }

/* The record link is the primary thing in its row. */
td a { color: var(--brand-2); font-weight: 600; text-decoration: none; }
td a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* Row-level actions: quiet until the row is touched, so 40 rows do not
   present 40 buttons, but the action is never actually hidden from the
   keyboard or from a touch device (no hover there). */
.row-actions { white-space: nowrap; text-align: right; }
@media (hover: hover) and (pointer: fine) {
  tbody tr .row-actions > * { opacity: .55; transition: opacity var(--t-fast) var(--ease-out); }
  tbody tr:hover .row-actions > *,
  tbody tr:focus-within .row-actions > * { opacity: 1; }
}

/* ---------------------------------------------------------------------------
   5b. Phone: a table becomes a list of records.

   Each row turns into a bordered block and each cell prints its own column
   name beside its value. The labels are copied out of <thead> by app.js at
   load, so this needs NO markup change on any of the 40-odd tables. With
   script off the current horizontal scroller is exactly what you get today,
   so there is no regression - only an upgrade when script runs.

   Only tables marked .tbl-stack participate. Arithmetic tables (the
   reconciliation, an invoice's totals) must stay as tables, because their
   columns are the point.
   ------------------------------------------------------------------------ */
/* SCREEN ONLY. A4 portrait at the declared 10mm margin gives a printable width
   of about 718 CSS px, which falls INSIDE this query - so every printed list
   silently inherited the phone layout. Measured 30 Aug 2026 with a real
   printToPDF: matters.php (60 rows) came out as 13 pages of bordered cards
   reading "AREALitigationSTAGEDiscoveryHANDLERWong Kar Meng", with page 1 about
   85% blank. Emulation.setEmulatedMedia("print") does not resize the page and
   therefore misses this entirely; the PDF is the only honest test. */
@media screen and (max-width: 760px) {
  .tbl-stack.is-stacked { min-width: 0; display: block; }
  .tbl-stack.is-stacked thead { display: none; }
  .tbl-stack.is-stacked tbody,
  .tbl-stack.is-stacked tr,
  .tbl-stack.is-stacked td { display: block; width: auto; }

  .tbl-stack.is-stacked tr {
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    padding: 10px 12px;
    margin-bottom: 10px;
    background: var(--card);
  }
  .tbl-stack.is-stacked tr:hover { background: var(--card); }
  .tbl-stack.is-stacked tr:last-child { margin-bottom: 0; }

  .tbl-stack.is-stacked td {
    border: 0;
    padding: 3px 0;
    display: grid;
    grid-template-columns: minmax(84px, 34%) minmax(0, 1fr);
    gap: 10px;
    align-items: baseline;
  }
  .tbl-stack.is-stacked td::before {
    content: attr(data-label);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .055em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* The first cell is the record's identity: it gets the full width and
     reads as the block's heading rather than as another labelled field. */
  .tbl-stack.is-stacked td:first-child {
    display: block;
    font-size: 15px;
    padding: 0 0 8px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--line-soft);
  }
  .tbl-stack.is-stacked td:first-child::before { content: none; }
  .tbl-stack.is-stacked td.num { text-align: left; }
  /* A cell with nothing in it is noise on a phone; drop the whole line. */
  .tbl-stack.is-stacked td.is-blank { display: none; }
}

/* ===========================================================================
   6. METRICS
   ===========================================================================
   A number with no context is not information. The old .stat was a bare
   count in a card: "0 / 0 / 6" over 300px of a phone screen, telling the
   user nothing they could act on. A metric now carries its own meaning and,
   where there is one, the action it implies.
   ------------------------------------------------------------------------ */

.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

.metric {
  display: block;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
  padding: 13px 15px 14px;
  text-decoration: none;
  color: inherit;
  position: relative;
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
/* A metric that links somewhere says so by lifting; one that does not, does
   not move. The affordance is the motion, so it never needs a "click here". */
a.metric:hover { border-color: #c3d1de; box-shadow: var(--e-2); transform: translateY(-1px); }
a.metric:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }

.metric-l {
  font-size: 11.5px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 7px;
}
.metric-n {
  font-size: 27px; font-weight: 640; letter-spacing: -.03em; line-height: 1;
  color: var(--ink); display: block;
}
.metric-sub { font-size: 12.5px; color: var(--muted); margin: 6px 0 0; line-height: 1.45; }

/* Severity is carried by a left edge and the figure, never by flooding the
   whole card - a wall of red cards is a wall, not a warning. */
.metric.bad  { border-left: 3px solid var(--bad); }
.metric.bad  .metric-n { color: var(--bad); }
.metric.warn { border-left: 3px solid var(--warn); }
.metric.warn .metric-n { color: var(--warn); }
.metric.ok   { border-left: 3px solid var(--ok); }

/* A metric whose value is a PHRASE, not a figure: "Ends in 9 days", "In
   grace", "No term recorded". The 27px numeral size is tuned for two or three
   digits and turns a six-word state into a wrapped headline, so the text
   variant steps down and lets the words breathe. In the system rather than
   inline on the one screen that needed it first (CR-2): the next state label
   that is a sentence must not have to invent its own font size. */
.metric-n.is-text {
  font-size: 18px; font-weight: 620; letter-spacing: -.01em; line-height: 1.25;
}

/* Zero is good news on an exception counter. Say so quietly rather than
   printing a big grey 0 that reads as "no data". */
.metric.is-zero .metric-n { color: var(--muted); font-weight: 600; }

@media (max-width: 700px) {
  .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .metric { padding: 11px 12px 12px; }
  .metric-n { font-size: 22px; }
  .metric-l { font-size: 10.5px; margin-bottom: 5px; }
  .metric-sub { font-size: 11.5px; }
}

/* ===========================================================================
   7. TOOLBAR / FILTERS
   ===========================================================================
   The filter row was a set of bare form fields with labels, so it had the
   same visual weight as the record form below it and the eye could not tell
   "narrow this list" from "fill this in". It is a recessed control strip now.
   ------------------------------------------------------------------------ */

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  margin-bottom: 14px;
  padding: 12px 14px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.toolbar .field { margin-bottom: 0; }
.toolbar label { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }
.toolbar input, .toolbar select { background: var(--card); }
.toolbar .grow { flex: 1 1 240px; min-width: 0; }

@media (max-width: 700px) {
  .toolbar { padding: 10px 11px; gap: 8px; }
  .toolbar .field { flex: 1 1 100%; }
}

/* ===========================================================================
   8. EMPTY STATES
   ===========================================================================
   Already used in 55 places, which is the good news. It just looked like an
   error - centred grey text in the middle of a bordered box. An empty list
   is the NORMAL state of a new firm, so it should read as an invitation.
   ------------------------------------------------------------------------ */

.empty { text-align: center; padding: 40px 24px; color: var(--ink-2); }
.empty p { margin: 0 auto 6px; max-width: 46ch; font-size: 14.5px; font-weight: 550; }
.empty p.muted { font-weight: 400; font-size: 13.5px; line-height: 1.55; }

/* ===========================================================================
   9. FLASH / BANNER
   ===========================================================================
   Kept in normal document flow rather than replaced with a floating toast:
   a toast would be a new persistent overlay, it would duplicate a mechanism
   that already works with script turned off, and this application's rule is
   that nothing floats over a ledger figure.
   ------------------------------------------------------------------------ */

.flash, .banner {
  border-radius: var(--r-sm);
  padding: 11px 14px;
  font-size: 13.5px;
  line-height: 1.5;
  border-width: 1px;
  border-style: solid;
  display: flex;
  gap: 10px;
  align-items: flex-start;
}
.flash { margin-bottom: 18px; }
/* A coloured edge reads faster than a tinted box, and survives a colour
   vision deficiency because the position carries the signal too. */
.flash-ok   { border-left-width: 3px; }
.flash-warn { border-left-width: 3px; }
.flash-bad  { border-left-width: 3px; }
.banner.info { background: var(--info-bg); border-color: var(--info-line); color: var(--info); }

/* ===========================================================================
   10. SUBNAV (the view tabs on a list screen)
   ===========================================================================
   Was a folder-tab metaphor whose active tab merged into the card below it.
   Once the card stopped being flush with it, the tabs read as broken. A
   segmented control is honest about what it is: a filter, not a location.
   ------------------------------------------------------------------------ */

.subnav {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  margin: 0 0 16px;
  padding: 3px;
  background: var(--surface-active);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.subnav a {
  border: 0;
  margin: 0;
  border-radius: var(--r-xs);
  padding: 6px 13px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: none;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.subnav a:hover { background: rgb(255 255 255 / .7); color: var(--ink); }
.subnav a[aria-current="page"] {
  background: var(--card);
  color: var(--brand);
  box-shadow: var(--e-1);
}
@media (max-width: 700px) {
  .subnav { display: flex; width: 100%; }
  .subnav a { flex: 1 1 auto; text-align: center; padding: 8px 10px; }
}

/* ===========================================================================
   11. SHELL
   ===========================================================================
   The sidebar is still a normal grid column that scrolls with the page. What
   changed is legibility of state: the active item was a 24%-white wash with
   an inset bar, which at a glance was hard to separate from hover. It now
   reads at a distance, and the group labels sit on their own baseline.
   ------------------------------------------------------------------------ */

.side { padding: 16px 10px 24px; gap: 18px; }
.brand { padding: 4px 8px 2px; }
.brand-mark { border-radius: var(--r-sm); }

.navgroup { margin: 0 0 4px; padding: 0 10px; color: rgb(255 255 255 / .55); }
.sidenav ul { margin: 0 0 16px; }

/* THE CONSOLE RAIL, WITH ITS GROUP HEADINGS DROPPED (27 Aug 2026), still comes
   out of the renderer as one <ul> PER GROUP - the groups are kept in the data
   for their order and for page_permission(), only their headings are not
   printed. Five lists each carrying the 16px that used to separate one heading
   from the next would put four gaps down a rail of five rows, which reads as
   five sections rather than as the single list it now is. So the gap goes with
   the heading it belonged to, and the last one keeps the space that separates
   the rail from whatever follows it. */
.sidenav ul.navflat { margin-bottom: 0; }
.sidenav ul.navflat:last-of-type { margin-bottom: 0; }
/* The 16px that used to close the flat list is now the rail's own 14px
   padding-bottom: measured 15 Sep 2026, twenty rows at 35px plus that gap plus
   the padding came to 729px in a 722px column at 1440x900, a 7px scroll that
   hid nothing and pushed the last row under a fade. Same breathing room, kept
   in one place. */
.sidenav a {
  border-radius: var(--r-sm);
  padding: 7px 10px;
  font-size: 13.5px;
  font-weight: 500;
  color: rgb(255 255 255 / .74);
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.sidenav a:hover { background: rgb(255 255 255 / .09); color: #fff; }
.sidenav a[aria-current="page"] {
  background: rgb(255 255 255 / .14);
  color: #fff;
  font-weight: 600;
  box-shadow: inset 2px 0 0 rgb(255 255 255 / .85);
}

/* --- Topbar ------------------------------------------------------------
   Was: a search box, a name, a role pill and a text link, all at the same
   weight, so nothing read as the primary control. Now search owns the row
   (it is the fastest way into any record), and identity collapses into one
   menu instead of three separate scraps of text.                        */

.topbar { padding: 10px 24px; gap: 12px; min-height: 56px; }
.gsearch input {
  border-radius: var(--r-sm);
  padding: 7px 12px 7px 34px;
  font-size: 13.5px;
  background: var(--surface-sunk) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235b6774' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E") no-repeat 10px center;
}
.gsearch input:focus { background-color: #fff; box-shadow: var(--focus); }

/* The keyboard hint on the search box. Teaches the shortcut without a tour. */
.kbd {
  display: inline-block;
  font: 600 10.5px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--muted);
  background: var(--card);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: var(--r-xs);
  padding: 0 5px;
  white-space: nowrap;
}

.who { gap: 8px; }
.who-name { font-size: 13px; font-weight: 600; color: var(--ink-2); }

/* ===========================================================================
   12. COMMAND PALETTE
   ===========================================================================
   A daily-use professional tool earns a keyboard entry point. This is a
   transient <dialog> the user opens deliberately, exactly like the
   assistant - not new persistent chrome. It navigates only; it performs no
   action of its own, so it can never become a second doorway to a form
   whose rules live somewhere else.

   DELIBERATELY NOT a .ls-dialog. The house modal anatomy (93dvh, fixed
   footer, Close bottom left, X top right) exists so the action buttons never
   move between one form dialog and the next. A palette has no action buttons
   at all, and a 560px-wide search box stretched to 93dvh would be absurd. So
   it is its own component with its own class, and the modal rule is left
   governing modals rather than bent to cover something that is not one.
   Esc closes it, which is the platform's own behaviour on <dialog>.
   ------------------------------------------------------------------------ */

.ls-cmdk {
  width: min(560px, 100vw - 32px);
  max-height: min(70dvh, 560px);
  padding: 0;
  margin: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--e-3);
  overflow: hidden;
}
.ls-cmdk[open] { display: flex; flex-direction: column; }
.ls-cmdk::backdrop { background: rgb(13 25 41 / .46); }
@media (prefers-reduced-motion: no-preference) {
  .ls-cmdk[open] { animation: ls-dlg-in var(--t-base) var(--ease-out); }
}
.cmdk-in {
  flex: none;
  border: 0;
  border-bottom: 1px solid var(--line-soft);
  border-radius: 0;
  padding: 15px 18px;
  font-size: 15.5px;
  width: 100%;
  background: var(--card);
}
.cmdk-in:focus { outline: none; box-shadow: none; border-color: var(--line-soft); }
.cmdk-list { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; padding: 6px; margin: 0; list-style: none; }
.cmdk-group {
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); padding: 10px 12px 4px; margin: 0;
}
.cmdk-list a {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: var(--r-sm);
  color: var(--ink); text-decoration: none; font-size: 14px;
}
.cmdk-list a[aria-selected="true"] { background: var(--brand-tint); color: var(--brand); font-weight: 600; }
.cmdk-list a .cmdk-grp { margin-left: auto; font-size: 11.5px; color: var(--muted); font-weight: 500; }
.cmdk-empty { padding: 26px 18px; text-align: center; color: var(--muted); font-size: 13.5px; }
.cmdk-foot {
  flex: none; border-top: 1px solid var(--line-soft); background: var(--surface-sunk);
  padding: 8px 14px; font-size: 11.5px; color: var(--muted);
  display: flex; gap: 14px; flex-wrap: wrap;
}

/* ===========================================================================
   13. DIALOGS
   ===========================================================================
   The anatomy (fixed header with X top right, scrolling body, fixed footer
   with Close bottom left) is unchanged - it is a house rule and it is right.
   What changes is that the surface now reads as a real material arriving:
   it scales and fades together over 170ms rather than appearing.
   ------------------------------------------------------------------------ */

.ls-dialog { border-radius: var(--r-md); box-shadow: var(--e-3); }
.ls-dialog::backdrop { background: rgb(13 25 41 / .46); }

@media (prefers-reduced-motion: no-preference) {
  .ls-dialog[open] { animation: ls-dlg-in var(--t-base) var(--ease-out); }
  .ls-dialog[open]::backdrop { animation: ls-bd-in var(--t-base) var(--ease-out); }
}
@keyframes ls-dlg-in {
  from { opacity: 0; transform: translateY(8px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes ls-bd-in { from { opacity: 0 } to { opacity: 1 } }

.dialog-actions { padding: 14px 22px; background: var(--surface-sunk); border-top: 1px solid var(--line); }

/* ===========================================================================
   14. SKELETON
   ===========================================================================
   Only for content that genuinely arrives after the page does: the AI
   answers and the live client balance. Every other screen here is rendered
   by the server in one pass, so a skeleton on it would be theatre.
   ------------------------------------------------------------------------ */

.skel { border-radius: var(--r-xs); background: var(--surface-active); position: relative; overflow: hidden; }
.skel-line { height: 11px; margin-bottom: 8px; }
.skel-line:last-child { margin-bottom: 0; width: 62%; }
@media (prefers-reduced-motion: no-preference) {
  .skel::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgb(255 255 255 / .72), transparent);
    animation: ls-shimmer 1.25s ease-in-out infinite;
  }
}
@keyframes ls-shimmer { from { transform: translateX(-100%) } to { transform: translateX(100%) } }

/* ===========================================================================
   15. RESPONSIVE AND MOTION GUARDS
   ========================================================================== */

@media (max-width: 900px) {
  .topbar { padding: 9px 14px; }
  .card { padding: 15px 16px; border-radius: var(--r-sm); }
  .card.flush > .card-head { padding: 12px 14px 10px; }
}

/* Reduced motion means gentler feedback, not none: the press state and the
   focus ring stay, the travel goes. */
@media (prefers-reduced-motion: reduce) {
  .ls-dialog[open], .ls-dialog[open]::backdrop, .ls-cmdk[open] { animation: none; }
  a.metric:hover { transform: none; }
  .btn:active:not(:disabled) { transform: none; }
  .skel::after { animation: none; }
}

/* Contrast preference: fills give way to defined edges. */
@media (prefers-contrast: more) {
  .toolbar, thead th, .subnav { background: var(--card); }
  .subnav a[aria-current="page"] { outline: 2px solid var(--brand); }
  .tag { outline: 1px solid var(--muted); }
}

@media print {
  /* A printed table keeps its columns and repeats its headers on every sheet. */
  .tbl-stack.is-stacked { display: table !important; min-width: 0 !important; }
  .tbl-stack.is-stacked thead { display: table-header-group !important; }
  .tbl-stack.is-stacked tbody { display: table-row-group !important; }
  .tbl-stack.is-stacked tr { display: table-row !important; margin: 0 !important;
      border: 0 !important; padding: 0 !important; }
  .tbl-stack.is-stacked td { display: table-cell !important; width: auto !important;
      grid-template-columns: none !important; }
  .tbl-stack.is-stacked td::before { content: none !important; }
  thead { display: table-header-group; }
  .toolbar, .subnav, .ls-cmdk, .metric-sub { display: none !important; }
  .metrics { display: flex; gap: 16px; }
  .card { box-shadow: none; }
  thead th { background: #fff; }
}

/* The search field and its shortcut hint share one row, so the hint never
   needs absolute positioning against an input whose width is fluid.
   Hidden until script runs (it would do nothing otherwise) and on a phone,
   where there is no keyboard to press it with. */
.gsearch { display: flex; align-items: center; gap: 8px; }
.gsearch input { flex: 1 1 auto; }
.gsearch-kbd { flex: none; cursor: pointer; }
.gsearch-kbd[hidden] { display: none; }
.gsearch-kbd:hover { color: var(--ink); border-color: #c3d1de; }
.cmdk-fallback[hidden] { display: none; }
@media (max-width: 760px), (pointer: coarse) { .gsearch-kbd { display: none; } }

/* The exceptions panel on the diary. Severity is a left edge and a pill, not
   a flooded card: four tinted red boxes stacked up read as a wall rather
   than as four separate things to do. */
.attn { padding: 0 0 0 13px; border-left: 3px solid var(--line); margin-bottom: 16px; }
.attn:last-child { margin-bottom: 0; }
.attn-critical { border-left-color: var(--bad); }
.attn-warning  { border-left-color: var(--warn); }
.attn-h { margin: 0 0 3px; display: flex; align-items: center; gap: 9px; flex-wrap: wrap; font-size: 14.5px; }
.attn-h strong { font-weight: 640; }
.attn-d { margin: 0; color: var(--ink-2); font-size: 13.5px; line-height: 1.5; max-width: 78ch; }
.attn-a { margin: 5px 0 0; font-size: 13px; }
.attn-a a { color: var(--brand-2); font-weight: 600; text-decoration: none; }
.attn-a a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ---------------------------------------------------------------------------
   STACKED TABLE: beat the six-column floor.

   `.table-scroll table:has(thead th:nth-child(6)){min-width:720px}` computes to
   (0,2,3) because :has() takes the specificity of its argument. The stacking
   rule was `.tbl-stack.is-stacked` at (0,2,0), so it LOST: the table went
   display:block but kept a 720px floor inside a 375px screen, and every record
   block overflowed its card by 345px. Measured on a 375x812 handset.

   (0,3,1) wins, and a media query adds no specificity of its own, so the
   selector has to carry it.
   ------------------------------------------------------------------------ */
@media (max-width: 760px) {
  .table-scroll table.tbl-stack.is-stacked,
  table.tbl-stack.is-stacked { min-width: 0; width: 100%; display: block; }

  /* A long matter title must wrap inside the block rather than push it wide. */
  table.tbl-stack.is-stacked td:first-child { overflow-wrap: anywhere; }
  table.tbl-stack.is-stacked td { overflow-wrap: anywhere; }

  /* The scroller has nothing left to scroll once the table stacks; leaving it
     scrollable let a stray drag shift the whole list sideways. */
  .table-scroll:has(> table.is-stacked) { overflow-x: visible; }
}

/* ---------------------------------------------------------------------------
   FILTER TOOLBAR ON A PHONE.

   Measured: on Matters at 375px the first actual matter began 739px down an
   812px viewport. Nine tenths of the screen was furniture - title, three
   action buttons, the view tabs, then four full-width filter rows.

   The search box keeps its own row because it is the one people use. The
   narrow selects pair up, and the Filter button sits with them instead of
   claiming a fifth row. Saves roughly 150px, which is one more matter visible
   before the fold.
   ------------------------------------------------------------------------ */
@media (max-width: 700px) {
  .toolbar { gap: 8px 10px; }
  .toolbar .field { flex: 1 1 calc(50% - 5px); min-width: 0; }
  .toolbar .grow, .toolbar .field:has(input[type="search"]) { flex: 1 1 100%; }
  .toolbar .field:has(> .btn), .toolbar .field:has(> button) { flex: 0 0 auto; align-self: flex-end; }
}

/* ---------------------------------------------------------------------------
   THREE REGRESSIONS INTRODUCED BY THE v2 PASS, FIXED. 7 Aug 2026.

   1. The restyled global search box was set to 13.5px. Two consequences, both
      real: iOS zooms the whole page whenever a field under 16px takes focus,
      and the placeholder ("Search clients, matters, people") sat below the
      contrast floor. The project already had a 16px rule for small screens and
      coarse pointers; `.gsearch input` is (0,1,1) and later in the file, so it
      quietly beat it. It is restored here rather than in the original rule so
      the reason stays attached to it.

   2. `.row-actions > * {opacity:.55}` was a hover-reveal flourish. Opacity
      multiplies against the background, so a button at 55% measured 3.86:1 -
      below AA - in its RESTING state, which is the state it is in almost all
      the time. Dimming a control to make a table look calmer is not worth
      making it hard to read. Removed; the actions are simply always legible.

   3. A stacked table is display:block, and that strips the implicit table,
      row and cell roles out of the accessibility tree, so a screen reader on
      a phone got a run of unlabelled text where the sighted user gets a
      labelled record. app.js now re-asserts the roles when it stacks.
   ------------------------------------------------------------------------ */

@media (max-width: 760px), (pointer: coarse) {
  .gsearch input { font-size: 16px; }
}
.gsearch input::placeholder { color: #626f7e; }   /* 4.86:1 on --s2 #f7f9fb */

@media (hover: hover) and (pointer: fine) {
  tbody tr .row-actions > * { opacity: 1; }
}

/* ===========================================================================
   LIST TABLES AT 1280px, AND THE SAME TABLES ON PAPER. 7 Aug 2026.
   ===========================================================================
   Four table problems that all come back to the same habit: a rule written
   for one screen and then left to apply to every screen that happens to share
   a class name.
   ------------------------------------------------------------------------ */

/* 1. Matters, Billing and Enquiries scroll sideways on a 1280px laptop the
      moment an advisory alert claims the 260px aside. Measured on Matters at
      1280px: 236px of sidebar and 56px of page padding leave 988px, the aside
      and its gap take 282px of that, and the six-column floor above then
      forces the table to 720px inside a 706px scroller. What ends up behind
      the scrollbar is `Next due`, which is the column the screen is read for.

      The floor is right. The columns are what should give. `hide-md` drops a
      column below 1400px, for the columns whose value the primary cell
      already repeats in its `.muted` sub-line. It has to out-specify
      `.tbl-stack.is-stacked td` (0,2,2), which sets display on a phone, so
      the selector carries that weight rather than reaching for !important. */
@media (max-width: 1399px) {
  .hide-md,
  th.hide-md, td.hide-md,
  .tbl-stack.is-stacked td.hide-md { display: none; }
}

/* 1b. The markup that carries `hide-md` is not written yet, and nothing may
       scroll sideways at 1280px in the meantime. Between 1280 and 1399px, and
       only where the aside is actually taking its 260px, the six-column floor
       gives way and the cells wrap instead: a wrapped date is a smaller loss
       than a date behind a scrollbar. Below 1280px the aside has already
       stacked and the floor is free to hold. (0,4,3) against the floor's
       (0,2,3). Once the columns carry the class this rule stops mattering,
       because the table will fit without it. */
@media (min-width: 1280px) and (max-width: 1399px) {
  .page:has(> .page-aside) .table-scroll table:has(thead th:nth-child(6)) { min-width: 0; }
}

/* 2. Printing clipped the right-hand columns of every report. `.table-scroll`
      is overflow-x:auto, and what a scroller merely hides on screen it loses
      outright on paper - there is no scrollbar to reach for. Measured on
      accountant_report.php: the Total column, the figure the report is signed
      off on, never printed at all. So on paper the scroller stops being one,
      the floors are lifted, and the type comes down to a size that lets the
      columns fit, with the narratives and the long client names taking the
      wrapping. */
@media print {
  .table-scroll { overflow: visible !important; }
  .table-scroll table,
  .table-scroll table:has(thead th:nth-child(6)) { min-width: 0 !important; }
  table { font-size: 9pt; }
  /* A COLUMN MAY NOT COLLAPSE TO A VERTICAL STRIP OF SINGLE CHARACTERS.
     `overflow-wrap: anywhere` is what makes a wide table fit the page, and it
     is also what tells the layout engine a column's minimum content width is
     one character. On an eight or nine column report that is exactly what
     happens. Measured under this stylesheet on a nine-column client account
     reconciliation, A4 at 794px: the Statement date and Account columns each
     came out 34.5px wide and 671.8px TALL - "01/08/2026" printed as ten lines
     of one character. The reader cannot read a date like that, and the date is
     what a reconciliation is filed against.

     A floor fixes it without giving up the fit. `break-word`, which was the
     other candidate, restores each column's natural minimum and pushed the
     same table 180.3px past the page: that trades an unreadable column for a
     clipped one, and the clipped one is the Total the report is signed off on -
     the defect the block above this was written to fix. Measured, both ways,
     before choosing. With the floor and the tightened print padding below the
     same table lands at 770.8px inside 794px, with the date column at 60.5px.
     8ch is the smallest floor that holds a d/m/Y date and a short reference. */
  th, td { white-space: normal; overflow-wrap: anywhere; min-width: 8ch; }
  /* Nine columns at 12px of side padding spend 216px of an A4 page on air,
     which is the width the collapsed columns needed. Paper has no hover and no
     touch target, so the row can be tighter there than on screen. */
  .ledger td, .ledger th { padding: 5px 7px; }
  /* Specificity does not save .money here: it is declared inside
     `@layer components` from the build, and an unlayered rule beats a layered
     one whatever its weight. Measured on paper before this line: 50,000.00
     broke after the comma. A figure and an entry number are short and must
     stay on one line; they are what the report is read for.

     A HEADING IS NOT A FIGURE. `.money` also lands on the `<th>` that
     money_th() emits, so "Unpresented" and "In transit" were pinned to one
     line and took 115.1px and 95.1px of the same page the data columns were
     being squeezed out of. The figures still never break; only the words
     above them do. */
  .money, .num, .ref, .running { white-space: normal; overflow-wrap: normal; }
  td.money, td.num, td.ref, td.running { white-space: nowrap; overflow-wrap: normal; }
  /* Paper has no width problem to solve, so a column hidden for the laptop
     comes back. `revert` restores the user-agent value, which is table-cell
     on a th or td and block anywhere else. */
  .hide-md { display: revert !important; }
}

/* 3. `.ledger` names a table's LOOK, and 22 tables wear it. The positional
      widths above - first two columns pinned to their content, third given
      40% - describe the cash book's Date / Entry / Narrative and nothing
      else, so Billing was handing 40% of the row to `Issued` and the
      invoice's receipt table had `Receipt` pinned to 1%.

      What actually identifies a cash book is that its second column is an
      entry number. `td.ref:nth-child(2)` appears on ledger.php:366 and
      ledger_client.php:124 and nowhere else - the other three `td.ref` cells
      in the codebase are all first in their row - so the rule can state its
      own precondition and no markup has to change. The reset matches the
      original selector's (0,2,1) and comes later; the scoped rule is (0,4,3)
      and beats both. */
.ledger th:nth-child(1), .ledger td:nth-child(1),
.ledger th:nth-child(2), .ledger td:nth-child(2) { width: auto; white-space: normal; }
.ledger th:nth-child(3), .ledger td:nth-child(3) { width: auto; }

.ledger:has(tbody td.ref:nth-child(2)) th:nth-child(1),
.ledger:has(tbody td.ref:nth-child(2)) td:nth-child(1),
.ledger:has(tbody td.ref:nth-child(2)) th:nth-child(2),
.ledger:has(tbody td.ref:nth-child(2)) td:nth-child(2) { width: 1%; white-space: nowrap; }
.ledger:has(tbody td.ref:nth-child(2)) th:nth-child(3),
.ledger:has(tbody td.ref:nth-child(2)) td:nth-child(3) { width: 40%; }

/* 4. Re-checked, nothing to change, recorded so the next pass does not have
      to measure it again:

      Row actions. The resting opacity is 1 - the block above this one restores
      it at equal specificity and later in the file, so it wins - and the
      buttons are .btn.small.secondary, which is var(--ink) #141c28 on white:
      17.12:1. Nothing is dimmed and nothing needs a colour. The one row-action
      variant that is not .secondary is .btn.ghost, already var(--ink-2)
      #3a4757 at 9.46:1. State is not carried on opacity anywhere here now.

      Global search. `@media (max-width:760px),(pointer:coarse){.gsearch input
      {font-size:16px}}` and the placeholder colour both sit after the v2
      `.gsearch input` rule; the first matches it at (0,1,1) and wins on order,
      the second is (0,1,2) against the shared `input::placeholder` (0,0,2) and
      wins on both. Verified in the cascade, not just present in the file.
      The measured placeholder ratio is corrected in place above: #626f7e on
      --s2 #f7f9fb is 4.86:1, and 5.13:1 against the #fff the field fills with
      on focus. Both clear AA. */

/* 1c. The other half of `hide-md`. A column may only be dropped if its value
       is still on the row, so the primary cell carries a `.show-md` sub-line
       that appears exactly when the columns disappear. Above 1400px it would
       be duplication, so it is hidden there; on a stacked phone table the
       cells print their own labels and it would be duplication again, so it is
       hidden there too. */
.show-md { display: none; }
@media (max-width: 1399px) { .show-md { display: block; } }
@media (max-width: 760px) { .tbl-stack.is-stacked .show-md { display: none; } }

/* A withdrawn choice in the segmented control. `.segmented label` sets its own
   display, which beats the user agent's [hidden] rule, so the hide has to be
   stated here. Used by the recording-money dialog to take away "Transfer to
   office" when an office account is selected, since that posting only exists
   on the client account. */
.segmented input[hidden], .segmented label[hidden] { display: none !important; }

/* Visually hidden, still read aloud. Used for the caption that names the
   reconciliation statement: a table that carries figures a person has to tell
   apart needs an accessible name, and printing one on screen would only repeat
   the card heading already above it. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Row headers in the reconciliation statement.
   The labels down the left of "The proof" are row headers, not data - a screen
   reader was reading five rows of two anonymous columns and the figures
   arrived attached to nothing. They are marked up as th[scope=row] for that,
   and this restores the ordinary cell appearance, because the global th rule
   is written for COLUMN headers (uppercase, 11px, muted) and would otherwise
   shrink the labels on the one screen whose job is telling figures apart. */
.recon-proof th[scope="row"] {
  text-transform: none;
  letter-spacing: normal;
  color: inherit;
  font-size: inherit;
  font-weight: 400;
  white-space: normal;
  text-align: left;
  vertical-align: top;
}

/* A MODAL IS ONE HEIGHT. IT DOES NOT SIZE TO ITS CONTENT. 31 Aug 2026.

   Alex: "fix them, memorize this, fix height for all modals, do not change
   height according to modal content."

   THIS REVERSES THE 14 AUG DECISION THAT USED TO LIVE HERE, and the note is
   summarised rather than deleted because its measurement was real: a dialog
   pinned at 93dvh drew New Invoice, which is two selects, as a 670px box with
   about 380px of empty white under the last field. That was the case for
   `fit-content`, and it has lost to a bigger one.

   WHAT SIZING TO CONTENT COST. Every modal became a different height, and the
   same modal became a different height AS YOU USED IT: the feedback queue grew
   and shrank as the filter changed, a form jumped the moment a validation
   message appeared under a field, and the footer - the one row holding Close
   and the action - moved every time. Empty space is quiet. A control that
   moves while you are reaching for it is not.

   THE HEIGHT NOW LIVES IN EXACTLY ONE PLACE. There were five competing
   declarations on .ls-dialog when this was written - here, the generic rule
   below, .assistant, .tabbed and .fb-lane - and the winner was not the last
   one. It was THIS one, because :not(.assistant):not(.record) outscores a bare
   .ls-dialog on specificity whatever the order, which is why edits to the
   others kept appearing to do nothing. That is the 119-redefinition problem
   the 19 Aug audit named, in miniature. Search MODAL HEIGHT at the end of this
   file; nothing else sets one. */

/* Flat line icons down the left menu.

   One icon per destination, drawn inline in layout.php on a 24 grid and
   stroked in currentColor, so it inherits the link's own colour in the rest,
   hover and active states without a second colour rule to keep in sync.

   `flex: none` matters: the sidebar link is already a flex row, and without it
   a long label ("Accountant's Report pack", which wraps on the mobile drawer)
   squeezes the icon narrower and the icons stop lining up. The fixed 18px box
   is what keeps every label on the same vertical line. */
/* display:flex is restated, not assumed. The stylesheet already carries a
   later `.sidenav a { display: block }` from the Tailwind build, which won on
   the desktop breakpoint and dropped every icon onto its own line above its
   label - measured in Chrome: computed display "block", 21 two-line rows. The
   reset also sets `svg { display: block }`, so the icon could not sit inline
   either. Declaring the row here, last in the file, settles both. */
.sidenav a { display: flex; align-items: center; gap: 10px; }
.sidenav a .navicon { flex: none; width: 18px; height: 18px; opacity: .78; }
.sidenav a:hover .navicon,
.sidenav a[aria-current="page"] .navicon { opacity: 1; }
.navlabel { min-width: 0; }

/* The same icon in the command palette, where the row is also a flex line and
   .cmdk-grp is pushed right with margin-left:auto. */
.cmdk-list a { display: flex; align-items: center; gap: 10px; }
.cmdk-list a .navicon { flex: none; width: 17px; height: 17px; opacity: .6; }
.cmdk-list a[aria-selected="true"] .navicon { opacity: 1; }

/* ---------------------------------------------------------------------------
   The account menu, top right.

   A <details> element, not a scripted popover: it opens, closes on Esc and
   closes on an outside click with no JavaScript at all, which is the rule the
   rest of this application already holds to. The panel is position:absolute,
   which is a transient menu the user opened deliberately - it is not sticky or
   fixed chrome, and it covers nothing until it is asked to.
   --------------------------------------------------------------------------- */
.usermenu { position: relative; }
.usermenu > summary {
  display: flex; align-items: center; gap: 8px;
  list-style: none; cursor: pointer; user-select: none;
  padding: 6px 8px; border-radius: var(--radius-s);
  min-height: 36px;
}
.usermenu > summary::-webkit-details-marker { display: none; }
.usermenu > summary:hover { background: var(--tint, rgb(13 41 65 / .05)); }
.usermenu > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.usermenu-chev {
  width: 8px; height: 8px; flex: none; margin-left: 2px;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  opacity: .5; transition: transform .12s ease-out;
}
.usermenu[open] .usermenu-chev { transform: rotate(225deg) translate(-1px, -1px); }
.usermenu-pop { padding-top: 6px;
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  min-width: 232px; padding: 6px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius-m, 10px); box-shadow: 0 12px 28px rgb(13 25 41 / .16);
}
.usermenu-head {
  padding: 8px 10px 10px; margin: 0 0 4px;
  border-bottom: 1px solid var(--line-soft, var(--line));
  font-size: 12px; color: var(--muted); word-break: break-all;
}
.usermenu-pop a, .usermenu-pop button {
  display: block; width: 100%; padding: 9px 10px; border-radius: var(--radius-s);
  font-size: 13.5px; color: var(--ink); text-decoration: none;
  background: none; border: 0; text-align: left; font-family: inherit; cursor: pointer;
}
.usermenu-pop a:hover, .usermenu-pop button:hover { background: var(--brand-tint); color: var(--brand); }

/* .usermenu-here AND .usermenu-heremark WERE HERE, AND ARE DELETED WITH THE
   markup that used them. The account menu no longer renders a "you are here"
   row with a tick - it renders nothing for the console the reader is already
   in - so these two rules and the masked check mark they drew have no selector
   left to match. Deleted rather than left in place: dead CSS in this file is
   how it reached 119 redefinitions, and a rule nobody can reach is a rule the
   next reader has to prove is unreachable. See usermenu_item(). */

.usermenu-out { border-top: 1px solid var(--line-soft, var(--line)); margin-top: 4px; padding-top: 10px; }

/* THE TWO ICON MENUS LEFT OF THE ACCOUNT MENU (Alex, 14 Sep 2026): Apps, then
   the bell, then the profile. Same <details> element, same popover, same
   close-on-look-away script; only the trigger is a 36px icon square. */
.usermenu.um-icon > summary { width: 36px; height: 36px; padding: 0; justify-content: center; color: var(--ink-2); }
.usermenu.um-icon > summary:hover { color: var(--brand); }
.um-glyph { display: inline-flex; width: 20px; height: 20px; }
.um-glyph svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.um-icon-btn {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-s); color: var(--ink-2); font-family: inherit;
}
.um-icon-btn:hover { background: var(--tint, rgb(13 41 65 / .05)); color: var(--brand); }
.um-icon-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.um-icon-btn[aria-expanded="true"] { background: var(--tint, rgb(13 41 65 / .05)); color: var(--brand); }
/* THE COUNT, NOT THE BELL, CARRIES THE COLOUR (Alex, 14 Sep 2026: "why the
   bell is different design"). The glyph stays the same quiet ink as the Apps
   icon beside it; a small count sits just off the glyph's top-right corner,
   clear of the bell's outline, coloured by the worst thing behind it. */
.um-bell-n {
  position: absolute; top: 3px; right: 3px; min-width: 15px; height: 15px; padding: 0 4px;
  border-radius: var(--r-xs); font-size: 10px; font-weight: 700; line-height: 15px; text-align: center;
  font-variant-numeric: tabular-nums; background: var(--ink-2); color: #fff;
  box-shadow: 0 0 0 2px var(--card, #fff); pointer-events: none;
}
.um-bell .um-glyph { transform: translate(-1px, 1px); }
.um-bell.lvl-bad .um-bell-n { background: var(--bad); }
.um-bell.lvl-warn .um-bell-n { background: var(--warn); }
.um-bell.lvl-unknown .um-bell-n { background: var(--sev-unknown); }
.ls-sheet-sec {
  margin: 6px 4px 0; font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.ls-sheet-sec:first-child { margin-top: 0; }
/* THE FIRM AT THE FOOT OF THE RAIL (14 Sep 2026). A sibling after the
   scrolling nav, so it holds the bottom of the rail in normal flow. Light on
   the navy, a hairline above, the name wrapping in full - it is the one line
   that says whose files these are. */
.side > .sidenav { flex: 1 1 auto; min-height: 0; }
.sidefirm {
  flex: 0 0 auto; display: flex; flex-direction: row; align-items: flex-start; gap: 9px;
  margin: 4px 2px 0; padding: 14px 10px 2px;
  border-top: 1px solid rgb(255 255 255 / .12);
}
/* 15 Sep 2026: the "LAW FIRM" caption became a building glyph in front of the
   name; .sidefirm-k is no longer rendered. */
.sidefirm-ico { flex: 0 0 auto; display: inline-flex; color: rgb(255 255 255 / .62); margin-top: 1px; }
.sidefirm-ico .ico { width: 16px; height: 16px; }
.sidefirm-k {
  font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: rgb(255 255 255 / .5);
}
.sidefirm-name { font-size: 13px; font-weight: 600; line-height: 1.35; color: rgb(255 255 255 / .92); overflow-wrap: anywhere; }
/* Below 900px the rail is a drawer: the firm shows with the menu, inside it. */
@media (max-width: 900px) { .sidefirm { display: none; } .navdrawer-toggle:checked ~ .sidefirm { display: flex; } }
.um-icon-pop { min-width: 240px; padding: 10px 12px 12px; }
/* The Apps chooser is a launcher panel (Alex's reference, 14 Sep 2026): a
   wide sheet with a 3-column grid of icon tiles, label under each. The grid
   is ready; until the first app exists the panel shows its empty line. */
.um-apps .um-icon-pop { width: min(360px, calc(100vw - 32px)); padding: 14px 16px 16px; }
.um-apps-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
.um-apps-grid a { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 14px 6px; text-align: center; font-size: 12.5px; color: var(--ink); border-radius: var(--radius-s); text-decoration: none; }
.um-apps-grid a:hover { background: var(--brand-tint); color: var(--brand); }
.um-apps-grid img, .um-apps-grid svg { width: 40px; height: 40px; }
.um-pop-title { margin: 0 0 6px; font-size: 13px; font-weight: 600; color: var(--ink); }
.um-empty { margin: 0; font-size: 13px; color: var(--muted); }


/* On a phone the name is already hidden (see the .who rules above), so the
   summary is the role pill plus the chevron and needs a real touch target. */
@media (max-width: 640px) {
  .usermenu-pop { min-width: 200px; }
}

/* 44 SQUARE ON A COARSE POINTER, the same treatment .dialog-x and .rec-tool
   already carry further down this file.

   The base rule above sets min-height 36px and no width at all, and on a phone
   the name and the seat pill inside the summary are display:none, so the whole
   control collapses to its chevron. Measured on diary.php at 375x812 signed in
   as sarah.tan@tanrajesh.com.my: 26 x 36, and it was the ONLY control in the
   topbar under the 44px floor.

   It matters more than an ordinary miss because of what is behind it. This
   summary is the sole route to Sign Out: the nav drawer carries 37 links and
   not one of them is a way out, so a lawyer handing their phone to a client or
   leaving a client's office has this 26px chevron or nothing. CR-3: on a coarse
   pointer 44px is a floor and density yields.

   Width only widens and the chevron is centred in it, so nothing else in the
   topbar moves; 44px still sits inside --topbar-h 56px. */
@media (pointer: coarse), (max-width: 900px) {
  .usermenu > summary { min-width: 44px; min-height: 44px; justify-content: center; }
}

/* ---------------------------------------------------------------------------
   Inside a firm that is not your own.

   Normal flow at the very top of the page, never fixed: the one thing this
   strip must not do is sit over a client-account figure. It is loud on purpose
   - the entire risk of being able to enter another practice is forgetting
   which practice you are in.
   --------------------------------------------------------------------------- */
.visiting {
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
  padding: 10px 28px;
  background: var(--warn-bg, #fff6e5);
  border-bottom: 1px solid var(--warn, #b4740b);
  color: var(--warn, #7a4e05);
  font-size: 13.5px;
}
.visiting p { margin: 0; flex: 1 1 260px; }
.visiting form { margin: 0; }
.visiting-dot {
  width: 9px; height: 9px; flex: none; border-radius: 50%;
  background: var(--warn, #b4740b);
}
@media (max-width: 900px) { .visiting { padding: 10px 16px; } }

/* A subscriber and the law firms it holds, on the Admin Console. One block per
   subscriber so two subscriptions never read as one long list of firms. */
.subcard { padding: 4px 0 18px; border-top: 1px solid var(--line-soft, var(--line)); margin-top: 18px; }
.subcard:first-of-type { border-top: 0; margin-top: 0; }
.subcard-head { display: flex; align-items: flex-start; gap: 12px; margin: 0 0 12px; }
.subcard-head h3 { margin: 0 0 2px; font-size: 15px; }
.subcard-head > span { margin-left: auto; flex: none; }

/* ---------------------------------------------------------------------------
   The sidebar scrolls, and now says so.

   .shell is height:100dvh with overflow hidden, so on a 13-inch laptop (800px
   of viewport) roughly five menu items sit below the fold with no scrollbar
   shown until the pointer is over them, no fade and no shadow: measured, a
   partner could not find "Firm admin" at all. These are scroll shadows drawn
   with background-attachment, so they appear only at the end that actually has
   more content and cost no script and no extra element.
   --------------------------------------------------------------------------- */
.side {
  background-image:
    linear-gradient(var(--brand-ink), rgb(13 41 65 / 0)),
    linear-gradient(rgb(13 41 65 / 0), var(--brand-ink)),
    linear-gradient(rgb(255 255 255 / .22), rgb(255 255 255 / 0)),
    linear-gradient(rgb(255 255 255 / 0), rgb(255 255 255 / .22));
  background-position: 0 0, 0 100%, 0 0, 0 100%;
  background-repeat: no-repeat;
  background-size: 100% 22px, 100% 22px, 100% 7px, 100% 7px;
  background-attachment: local, local, scroll, scroll;
}
/* The drawer is in normal flow below 900px and does not scroll on its own,
   so the shadows would be permanently painted over the first and last item. */
@media (max-width: 900px) { .side { background-image: none; } }

/* The scope line in the assistant, stating which records the conversation may
   read. Quiet, but never absent in matter scope. */
.asst-scope {
  margin: 0; padding: 8px 22px;
  background: var(--brand-tint); color: var(--brand);
  font-size: 12.5px; border-bottom: 1px solid var(--line);
}

/* ---------------------------------------------------------------------------
   The AI suggestion panel (lib/ai_ui.php).

   One appearance for every AI feature in the application. A lawyer has to be
   able to tell at a glance which words are the firm's record and which are a
   machine's suggestion about it, and that has to look the same on the conflict
   screen, the bill and the STR register or it stops being a distinction.

   Deliberately NOT a card: a card is what the firm's own records are drawn in.
   This is a tinted panel with a sparkle mark and a standing caveat line that
   cannot be dismissed while the text is on screen.
   --------------------------------------------------------------------------- */
.aibox { margin: 14px 0 0; }
.aibox-go { display: inline-flex; align-items: center; gap: 7px; }
.aibox-go .navicon { width: 15px; height: 15px; flex: none; opacity: .85; }
.aibox-hint { margin: 6px 0 0; max-width: 64ch; }

.aibox-out {
  margin-top: 10px; padding: 12px 14px;
  border: 1px solid var(--brand-tint); border-left: 3px solid var(--brand);
  border-radius: var(--radius-s); background: var(--brand-tint);
}
.aibox-out.aibox-bad { border-color: var(--warn-bg); border-left-color: var(--warn); background: var(--warn-bg); }

/* WAITING IS NOT AN ANSWER, SO IT MUST NOT WEAR THE ANSWER'S SURFACE.
   lib/ai_ui.php puts data-state="idle|busy|answer|error" on this wrapper and
   says in its own comment that it is there so this file can give busy and
   error their own treatment. Error got one; busy never did. Measured on a
   fixture of the three states at 1000px: busy and answer were identical to the
   pixel - background rgb(234,240,246), brand left rail rgb(18,57,95), brand tag
   colour - and only the words in the tag differed. On the 4B backend a drafting
   action runs ten to twenty-five seconds, so that panel sits there looking like
   a finished AI suggestion for most of half a minute.
   Gate 4.B requires inference to be structurally separate from everything that
   is not inference, on a channel that is not the label alone. Busy therefore
   drops to the ordinary recessed surface with a neutral rail: the brand tint
   and the brand rail now mean "the model has answered" and nothing else.
   No motion: the tag says "Working. This is not an answer yet", and a
   pulsing panel on a screen a fee earner keeps open all day is a cost. */
.aibox[data-state="busy"] .aibox-out {
  border-color: var(--line); border-left-color: var(--muted); background: var(--surface-sunk);
}
.aibox[data-state="busy"] .aibox-tag { color: var(--ink-2); }

.aibox-tag {
  display: flex; align-items: center; gap: 7px;
  margin: 0 0 8px; font-size: 11.5px; letter-spacing: .02em;
  text-transform: uppercase; color: var(--brand);
}
.aibox-out.aibox-bad .aibox-tag { color: var(--warn); }
.aibox-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }

/* white-space:pre-wrap, because these answers are lists and short paragraphs
   and the model's own line breaks are the structure. */
.aibox-text {
  white-space: pre-wrap; font-size: 14px; line-height: 1.55; color: var(--ink);
  max-width: 74ch;
}
.aibox-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.aibox-out[hidden] { display: none; }

/* The paste-a-letter reader inside the undertakings dialog. A <details> so it
   costs nothing until it is wanted, and adds no height to a dialog whose job
   is the form above it. */
.ai-reader { margin: 14px 0 0; border-top: 1px solid var(--line-soft, var(--line)); padding-top: 12px; }
.ai-reader > summary { cursor: pointer; font-size: 13.5px; color: var(--brand); list-style: none; }
.ai-reader > summary::-webkit-details-marker { display: none; }
.ai-reader > summary:before { content: "+ "; font-weight: 600; }
.ai-reader[open] > summary:before { content: "- "; }
.ai-reader-body { padding-top: 10px; }
.ai-reader-body textarea { width: 100%; }

/* ---------------------------------------------------------------------------
   THE ALERT COLUMN AND THE TOP BAR ARE IN NORMAL FLOW. NOTHING HERE IS STICKY.

   This is the whole of what was once three sticky rules and, two hundred and
   fifty lines further down, three more rules reverting every one of them - the
   file arguing with itself in two places, with the argument FOR sticky written
   beside rules that had not applied since the day the revert was appended. A
   reader could open either half and come away believing the opposite of what
   ships. Consolidated into the single statement that is true.

   The house rule bans position:sticky and position:fixed, and it is not scoped
   to "unless the author judges it harmless". The exemption the aside had a
   case for - it is its own 300px grid track, so it covers nothing - was
   refused deliberately, because a rule that holds except where the author
   judges otherwise is not a rule, and because the top bar's case failed on its
   own merits anyway: 56px of search box held permanently in front of somebody
   reading a long ledger, on a product whose argument is density.

   What the alert column is for is answered by it being BESIDE the record
   rather than above it, and that is unchanged.

   WHAT IS KEPT, AND WHY IT IS NOT A LEFTOVER:
     - .main's scroll-padding-top, so an anchored jump ("Show me" on an alert)
       lands clear of the top of the scroll container rather than flush.
     - align-self, overscroll-behavior and scrollbar-width on the column. They
       were written for the sticky version and are still correct without it:
       the column sits at the top of its track rather than stretching, and the
       reserved scrollbar gutter stops a sideways jump. Removing them would
       change what ships, and this consolidation deliberately changes nothing
       that is on the screen.
   --------------------------------------------------------------------------- */
/* SUPERSEDED FOR THE TOP BAR, 11 Sep 2026. Alex asked, explicitly, for the
   visiting strip and the top bar to stay at the top while the page scrolls:
   the search, Ask LawServa and the account menu must always be in reach, and
   so must the reminder of whose firm this is. The owner's instruction outranks
   the house default. The alert column stays in normal flow as argued above.

   One wrapper, .topchrome, holds both rows and is the only sticky element.
   It sticks inside .main (the scroll container at 901px and up) and to the
   viewport below that, where .main is overflow:visible and the window
   scrolls. Not applied inside the record overlay (.shell-modal), whose own
   dialog chrome is already fixed. scroll-padding-top clears it for anchored
   jumps so "Show me" never lands underneath it. */
:root { --topbar-h: 56px; }

.shell:not(.shell-modal) .topchrome {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg);
}
.topchrome > .topbar { background: #fff; }
.shell:not(.shell-modal) .main { scroll-padding-top: calc(var(--topbar-h) + 60px); }
@media (max-width: 900px) {
  html:has(.shell:not(.shell-modal) .topchrome) { scroll-padding-top: 150px; }
}

@media (min-width: 901px) {

  .page:has(> .page-aside) > .page-aside {
    align-self: start;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
}

/* The role matrix on the Admin Console. A tick grid is read by scanning a
   column, so the ticks are centred and the row heading carries the weight. */
.matrix th { white-space: nowrap; }
.matrix th[scope="row"] {
  text-transform: none; letter-spacing: normal; font-size: 14px;
  font-weight: 400; color: var(--ink); text-align: left; white-space: normal;
}
.matrix td { text-align: center; }
.matrix td.yes { color: var(--ok, #1f7a4d); font-weight: 700; }
.matrix td.no  { color: var(--line); }

/* ---------------------------------------------------------------------------
   THE TABS INSIDE A MODAL ARE GONE. 28 Aug 2026.

   Alex, of the Owner Console's firm record - the only dialog that ever had
   them: "No need 3 tab pages, combine into one."

   `.modaltabs`, its radios, its `.tabpanel` children and the six `:has()`
   rules that drove them are deleted rather than left unreferenced, because a
   selector nothing emits is a promise this stylesheet cannot keep - the next
   person to write a tabbed dialog would find the rules, use them, and discover
   the markup contract only from the missing behaviour.

   `.segmented` itself STAYS. It is the ledger's posting filter and the record
   overlay's own control; the tabs only borrowed it.
   --------------------------------------------------------------------------- */

/* A table cell whose text is the control that opens the record. Looks like the
   link it behaves as, but it is a button, because it opens a dialog rather
   than going anywhere. */
.linkbtn {
  background: none; border: 0; padding: 0; font: inherit; text-align: left;
  color: var(--brand-2); font-weight: 600; cursor: pointer;
}
.linkbtn:hover { text-decoration: underline; }
.linkbtn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* A plain list that is not a set of ticks - used for what a firm still needs. */
.tickless { margin: 0 0 14px; padding-left: 20px; font-size: 14px; line-height: 1.6; }

/* ---------------------------------------------------------------------------
   The record overlay gets the alert column back.

   On the full page the alerts for a file sit in their own 300px column beside
   it. In the overlay they did not: the frame is as wide as the dialog, the
   dialog was 1180px, and the column only starts at 1280px - so inside the
   modal the same alerts fell back to full-width banners stacked above the
   record. That is the layout the alert column was built to replace. It pushed
   "File" and "Deadlines and dates" below the fold, and on a matter with an
   overdue mention AND a limitation date the reader saw two red boxes and no
   file at all.

   Two changes, no new component and no new copy:

   1. The dialog is wider - 1440px - so a two-column record plus a 300px alert
      column all fit without the deadline table going back into a scroller.
   2. Inside the frame the column starts at 1040px of frame width rather than
      1280px, because the frame carries no sidebar: every pixel of it is the
      record. Below that it stacks above the content exactly as it does on a
      phone, which is the correct behaviour in one column.

   Nothing here is sticky either, for the reason set out above the page-level
   rules: the ban is on the technique, not on the cases where it happens to
   cover nothing.
   --------------------------------------------------------------------------- */
/* WIDTH IS NOT SET HERE ANY MORE - see the single rule at the end of this
   file. It was set in two places, 1440px here and 1600px there, and only
   the later one ever applied; the first was dead code that read like the
   answer. One declaration, one number. */

@media (min-width: 1040px) {
  .shell-modal .page:has(> .page-aside) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    align-items: start;
    gap: 0 22px;
  }
  .shell-modal .page:has(> .page-aside) > * { grid-column: 1; }
  .shell-modal .page:has(> .page-aside) > .page-aside {
    grid-column: 2;
    grid-row: 2 / 9999;
    margin-bottom: 0;
    overscroll-behavior: contain;
  }
}

/* Under that width the frame is one column and the column sits in normal flow
   above the content, so it needs its own space beneath it. */
@media (max-width: 1039px) {
  .shell-modal .page:has(> .page-aside) > .page-aside { margin-bottom: 18px; }
}

/* ---------------------------------------------------------------------------
   The record's own AI opener ("Ask AI" on a matter).

   It carries the same sparkle mark as every ai_button(), because a fee earner
   has to be able to tell an AI affordance from an ordinary action without
   reading the label - and until now this one was the only AI control in the
   application drawn as plain text. Same anatomy as .aibox-go, so the two read
   as the same family at the two sizes they appear in.
   --------------------------------------------------------------------------- */
.askbtn { display: inline-flex; align-items: center; gap: 7px; }
.askbtn .navicon { width: 15px; height: 15px; flex: none; opacity: .85; }

/* ---------------------------------------------------------------------------
   COURT BUNDLE ROWS

   One document, one form. It is not a table: a form cannot legally straddle
   table cells, so each row carries its own labelled fields.

   The marks are short by nature (Part "A", page "12", exhibit "P1"), so the
   inputs are sized to their content rather than stretched across the card -
   a full-width box for two characters reads as though more is expected. At
   375px the row wraps instead of pushing its own contents past the card edge,
   which is what the flex defaults did.
   --------------------------------------------------------------------------- */
.bundle-row {
  margin: 12px 0 0;
  padding: 12px 14px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
}
.bundle-doc { margin: 0 0 10px; }
.bundle-doc .muted { font-size: 13px; }
.bundle-row .toolbar { flex-wrap: wrap; gap: 10px 14px; align-items: flex-end; }
.bundle-row .field { flex: 0 0 auto; min-width: 0; }
.bundle-row .field input { max-width: 11ch; }
.bundle-row .checkline { flex: 0 0 auto; min-width: 0; }

/* ---------------------------------------------------------------------------
   ASSISTANT COMPOSER: chrome, not body.

   `.ls-dialog.assistant > form { flex: 1 1 auto }` dates from when the form
   WAS the conversation. It is now the composer at the foot of the dialog, and
   it is the only <form> the assistant dialog has - so that rule made the box
   holding one input grow to fill every pixel the log did not want. Measured on
   an 824px dialog: the composer took 272px for about 90px of content, leaving
   the ~180px dead band between the input and the footer.

   The log is the part that should absorb the space; it already asks to
   (`.asst-log { flex: auto }`).
   --------------------------------------------------------------------------- */
.ls-dialog.assistant > form.asst-form { flex: 0 0 auto; }

/* ---------------------------------------------------------------------------
   FEEDBACK: reporting a problem, and the conversation about it.

   The thread is deliberately NOT a chat transcript with avatars and tails. It
   is a record of what a firm told its supplier and what the supplier answered,
   read months later by somebody deciding whether to report the next one - so
   it reads as a document, with the speaker and the time on every entry.
   --------------------------------------------------------------------------- */
.navcount {
  margin-left: auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent, #16406b);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  line-height: 18px;
}
.fb-thread { display: flex; flex-direction: column; gap: 14px; }
.fb-msg {
  padding: 12px 14px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  background: var(--card);
}
/* The vendor's replies are tinted so a firm can see at a glance whether
   anybody has answered. Never colour alone: the name and the LawServa pill
   carry the same fact in text. */
.fb-msg.fb-vendor { border-left: 3px solid var(--accent, #16406b); }
.fb-msg.fb-status { background: var(--bg); }
.fb-who { margin: 0 0 6px; font-size: 13px; font-weight: 600; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.fb-who .muted { font-weight: 400; }
.fb-body { margin: 0; white-space: pre-wrap; }

.fb-upload { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 12px; }
.fb-shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.fb-shots img,
.fb-shot img {
  display: block;
  max-width: 220px;
  max-height: 160px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
}
.fb-shot { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }

/* A container accepting a dropped screenshot says so while the file is over
   it. Outline rather than a border swap, so nothing on the form moves: a
   layout that shifts under the pointer is how a drop lands in the wrong place. */
.is-dropping { outline: 2px dashed var(--accent, #16406b); outline-offset: 4px; }

/* The screenshot marker. The canvas is the whole control: no palette, no
   toolbar, one tool. It scales to the dialog and never exceeds it, because a
   picture wider than its box is one you have to scroll to draw on. */
.mark-wrap { display: flex; justify-content: center; }
.mark-wrap canvas {
  max-width: 100%;
  height: auto;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  cursor: crosshair;
  touch-action: none;   /* the drag draws a box; it must not scroll the dialog */
}
.fb-shot { gap: 6px; }
.fb-shot .btn { width: 100%; }

/* ==========================================================================
   SAFE CUSTODY
   ========================================================================== */

/* The non-colour channel on every custody state pill. Colour alone is never
   the signal: a person who cannot tell the amber pill from the red one still
   reads "> Out" and "! Overdue", and so does a black and white printout. */
.state-glyph {
  display: inline-block; width: 1em; text-align: center;
  font-weight: 700; font-family: var(--font-mono, ui-monospace, monospace);
  opacity: .85;
}

.state-legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.state-legend li { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; font-size: 13.5px; }

/* ---------- safe custody labels ------------------------------------------
   Browser print-to-PDF, per lib/export.php. No PDF library, no barcode.
   Two labels across on A4 is the sheet a KL firm's box files actually take.
   -------------------------------------------------------------------------- */

.label-page { padding: 24px 24px 56px; background: var(--bg, #f6f8fa); }
.label-head {
  display: flex; gap: 18px; align-items: flex-start; justify-content: space-between;
  flex-wrap: wrap; margin-bottom: 22px;
}
.label-head h1 { font-size: 20px; margin: 0 0 4px; }
.label-head-acts { display: flex; gap: 8px; flex-wrap: wrap; }
.label-note {
  margin-top: 8px; font-size: 13px;
  background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn);
  border-radius: 8px; padding: 8px 12px;
}

.label-sheet { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6mm; }
.label {
  border: 1px solid var(--line); border-radius: 4px; padding: 6mm; background: #fff;
  min-height: 38mm; break-inside: avoid; page-break-inside: avoid;
  display: flex; flex-direction: column; gap: 2mm;
}
.label p { margin: 0; }
.label-ref {
  font-size: 16pt; font-weight: 700; letter-spacing: .02em;
  font-variant-numeric: tabular-nums; line-height: 1.15;
}
.label-desc { font-size: 10pt; line-height: 1.3; }
.label-meta { font-size: 8pt; color: var(--muted); }
.label-foot {
  margin-top: auto; padding-top: 2mm; font-size: 7.5pt; color: var(--muted);
  display: flex; justify-content: space-between; gap: 8px;
}

@media (max-width: 700px) {
  .label-sheet { grid-template-columns: 1fr; }
}

@media print {
  @page { size: A4; margin: 10mm; }
  .label-page { padding: 0; background: #fff; }
  .label-sheet { gap: 0; }
  .label { margin: 0; border-radius: 0; border-color: #000; }
  /* Grey does not survive a laser printer, and a label nobody can read is
     worse than no label. */
  .label-meta, .label-foot { color: #000; }
}

/* ---------------------------------------------------------------------------
   Diary counters: the UNKNOWN state.

   `is-zero` says "we looked and there is nothing". `is-unknown` says "there is
   nothing to look at", which is a different fact and must not be able to be
   mistaken for the first at a glance. So it is not a coloured variant of the
   zero state: the figure position carries a question mark instead of a numeral,
   the left edge is a dashed rule rather than a solid one, and the subtitle
   leads with a bold sentence. Colour is doing none of the work (Gate A) - the
   glyph and the border style survive greyscale, a monochrome print and a
   screen reader, which reads the visually-hidden "Not known" in the figure's
   place.
   ------------------------------------------------------------------------ */
.metric.is-unknown { border-left: 3px dashed var(--muted); }
.metric.is-unknown .metric-n { color: var(--muted); font-weight: 700; }
.metric.is-unknown .metric-sub strong { color: var(--ink); }

/* ---------------------------------------------------------------------------
   The firm name in the topbar, at the widths where the sidebar stops showing
   it. See the note in lib/layout.php: on a phone the application named the
   tenant nowhere, and knowing which practice you are posting into is not
   optional in a system that holds client money.
   ------------------------------------------------------------------------ */
.topfirm { display: none; }

@media (max-width: 900px) {
  .topfirm {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
    /* Truncates rather than wrapping: the topbar is one line, and a firm name
       that pushes the search box off the row costs more than it gives. */
    max-width: 46vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ==========================================================================
   The practice calendar.

   Appended by hand, like everything below the Tailwind build at the top of
   this file. Written as explicit CSS rather than as utility classes on the
   markup, because a utility the build never emitted does not exist and fails
   silently, and a diary that silently loses its severity treatment is exactly
   the class of defect this screen exists to prevent.

   SEVERITY IS A RESERVED RAMP. The five values below own their hues. A tenant
   re-themes --brand; nothing re-themes these, because what a colour MEANS on a
   deadline screen is a data-integrity question and not a branding one. Every
   one of them is paired with a persistent glyph and an accessible label in the
   markup, so the colour is the third channel and never the only one.
   ========================================================================== */

:root {
  --sev-critical:      #983030;
  --sev-critical-bg:   #fbeceb;
  --sev-critical-line: #e2a9a6;
  --sev-warning:       #8a5710;
  --sev-warning-bg:    #fdf2e0;
  --sev-warning-line:  #e6c78e;
  --sev-info:          #1b5488;
  --sev-info-bg:       #eaf0f6;
  --sev-info-line:     #b9cde0;
  --sev-normal:        #4d5966;
  --sev-normal-bg:     #f1f3f6;
  --sev-normal-line:   #d5dbe2;
  /* Not a shade of grey standing in for "we do not know". A distinct, visibly
     unresolved treatment, because "no owner" and "no date fixed" are real
     answers and must not read as ordinary. */
  --sev-unknown:       #5d4d78;
  --sev-unknown-bg:    #f1eef7;
  --sev-unknown-line:  #cbc0de;
  --cal-closed-bg:     #eef1f5;
}

/* --- the surface ---------------------------------------------------------- */

.cal-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 14px; }
.cal-bar .cal-period { font-size: 17px; font-weight: 700; margin: 0 4px 0 0; }
.cal-nav { display: flex; gap: 6px; }
.cal-nav a {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 38px; padding: 0 12px;
  border: 1px solid var(--line); border-radius: var(--radius-s);
  background: var(--card); color: var(--ink-2); text-decoration: none; font-size: 14px;
}
.cal-nav a:hover { background: var(--line-soft); color: var(--ink); }

/* The legend. Persistent, on the surface, never a tooltip: CR-4. */
.cal-legend {
  display: flex; flex-wrap: wrap; gap: 6px 16px;
  margin: 0 0 14px; padding: 10px 14px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); font-size: 12.5px; color: var(--ink-2);
}
.cal-legend span { display: inline-flex; align-items: center; gap: 6px; }

.cal-glyph {
  flex: none; width: 13px; text-align: center;
  font-size: 12px; line-height: 1; font-weight: 700;
}
.sev-critical .cal-glyph, .cal-legend .sev-critical { color: var(--sev-critical); }
.sev-warning  .cal-glyph, .cal-legend .sev-warning  { color: var(--sev-warning); }
.sev-info     .cal-glyph, .cal-legend .sev-info     { color: var(--sev-info); }
.sev-normal   .cal-glyph, .cal-legend .sev-normal   { color: var(--sev-normal); }
.sev-unknown  .cal-glyph, .cal-legend .sev-unknown  { color: var(--sev-unknown); }

/* --- the month grid ------------------------------------------------------- */

/* A real table with real header cells. The month is tabular data - seven
   columns of weekdays - and an ARIA grid faked out of divs would be a worse
   version of what the element already does. */
.cal-month { width: 100%; border-collapse: collapse; table-layout: fixed; }
.cal-month th {
  padding: 8px 6px; font-size: 12px; font-weight: 700; letter-spacing: .3px;
  text-transform: uppercase; color: var(--muted); text-align: left;
  border-bottom: 1px solid var(--line);
}
.cal-month td {
  vertical-align: top; padding: 0; border: 1px solid var(--line-soft);
  /* FIXED height, and no scrolling inside a cell. At 300 events a month and
     20 on one day a scrolling cell hides the very thing the grid is for; the
     cell summarises and the day view carries the detail. */
  height: 118px; overflow: hidden;
}
.cal-month td.is-closed { background: var(--cal-closed-bg); }
.cal-month td.is-today  { outline: 2px solid var(--brand-2); outline-offset: -2px; }
.cal-month td.is-outside { background: var(--bg); }

.cal-cell { display: block; padding: 6px 7px; height: 100%; text-decoration: none; color: inherit; }
.cal-cell:hover { background: #fff; box-shadow: inset 0 0 0 1px var(--line); }
.cal-cell:focus-visible { outline: 3px solid var(--brand-2); outline-offset: -3px; }
.cal-daynum { font-size: 13px; font-weight: 700; color: var(--ink); }
.cal-month td.is-outside .cal-daynum { color: var(--muted); font-weight: 500; }
.cal-closed-name {
  display: block; font-size: 10.5px; line-height: 1.25; color: var(--warn);
  margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal-counts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
.cal-count {
  font-size: 10.5px; font-weight: 700; line-height: 1;
  padding: 2px 5px; border-radius: 3px; border: 1px solid;
}
.cal-count.sev-critical { color: var(--sev-critical); background: var(--sev-critical-bg); border-color: var(--sev-critical-line); }
.cal-count.sev-warning  { color: var(--sev-warning);  background: var(--sev-warning-bg);  border-color: var(--sev-warning-line); }
.cal-count.sev-info     { color: var(--sev-info);     background: var(--sev-info-bg);     border-color: var(--sev-info-line); }
.cal-count.sev-normal   { color: var(--sev-normal);   background: var(--sev-normal-bg);   border-color: var(--sev-normal-line); }
.cal-count.sev-unknown  { color: var(--sev-unknown);  background: var(--sev-unknown-bg);  border-color: var(--sev-unknown-line); }

/* --- the chip: one primitive, four views ---------------------------------- */

/* The signature of this screen, and it is compositional rather than
   decorative: a 3px severity rule down the left of every chip, in every view,
   at every density. It survives a tenant re-theme because the rule is the
   shape and not the colour. */
.cal-chip {
  display: flex; align-items: baseline; gap: 5px;
  margin-top: 3px; padding: 2px 5px 2px 6px;
  border-left: 3px solid var(--sev-normal);
  background: var(--sev-normal-bg);
  border-radius: 0 3px 3px 0;
  font-size: 11.5px; line-height: 1.3;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.cal-chip.sev-critical { border-left-color: var(--sev-critical); background: var(--sev-critical-bg); }
.cal-chip.sev-warning  { border-left-color: var(--sev-warning);  background: var(--sev-warning-bg); }
.cal-chip.sev-info     { border-left-color: var(--sev-info);     background: var(--sev-info-bg); }
.cal-chip.sev-unknown  { border-left-color: var(--sev-unknown);  background: var(--sev-unknown-bg); }
.cal-chip .cal-time { font-variant-numeric: tabular-nums; font-weight: 700; flex: none; }
.cal-chip .cal-what { overflow: hidden; text-overflow: ellipsis; }
/* A date fixed by statute is drawn differently on purpose, and nothing on this
   screen offers to move one. */
.cal-chip.is-fixed { border-left-style: double; border-left-width: 5px; }
.cal-chip.is-done  { opacity: .72; text-decoration: line-through; }
/* Never diarised. Dashed, because the obligation is real and the diary entry
   is not. */
.cal-chip.is-undiarised { border-left-style: dashed; }
.cal-more {
  display: block; margin-top: 4px; font-size: 11px; font-weight: 700;
  color: var(--brand-2);
}

/* --- day, week and agenda ------------------------------------------------- */

.cal-daylist { list-style: none; margin: 0; padding: 0; }
.cal-daylist > li { border-top: 1px solid var(--line-soft); padding: 10px 2px; }
.cal-daylist > li:first-child { border-top: 0; }
.cal-event { display: flex; gap: 10px; align-items: flex-start; }
.cal-event .cal-rule { flex: none; width: 4px; align-self: stretch; border-radius: 2px; background: var(--sev-normal); }
.cal-event.sev-critical .cal-rule { background: var(--sev-critical); }
.cal-event.sev-warning  .cal-rule { background: var(--sev-warning); }
.cal-event.sev-info     .cal-rule { background: var(--sev-info); }
.cal-event.sev-unknown  .cal-rule { background: var(--sev-unknown); }
.cal-event .cal-body { min-width: 0; flex: 1; }
.cal-event h4 { font-size: 14px; margin: 0 0 2px; font-weight: 600; }
.cal-meta { font-size: 12.5px; color: var(--muted); margin: 2px 0 0; }
.cal-meta strong { color: var(--ink-2); font-weight: 600; }
.cal-when { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 52px; flex: none; font-size: 13px; }
.cal-daysep {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 22px 0 6px; padding-bottom: 5px; border-bottom: 1px solid var(--line);
}
.cal-daysep h3 { font-size: 14px; margin: 0; font-weight: 700; }
.cal-daysep:first-child { margin-top: 0; }

/* A clash is not a colour. It is a labelled, bordered notice on both events. */
.cal-clash {
  margin-top: 6px; padding: 6px 9px; font-size: 12px;
  border: 1px solid var(--sev-critical-line); border-left-width: 4px;
  background: var(--sev-critical-bg); color: var(--sev-critical); border-radius: 4px;
}
.cal-landing {
  margin-top: 6px; padding: 6px 9px; font-size: 12px;
  border: 1px solid var(--sev-warning-line); border-left-width: 4px;
  background: var(--sev-warning-bg); color: var(--sev-warning); border-radius: 4px;
}

/* --- the honesty panel ---------------------------------------------------- */

/* An empty month with two hundred undiarised files is not an empty month, and
   this is the block that says so. Deliberately not styled as a dismissible
   toast: it is a statement about what the firm does NOT know. */
.cal-truth {
  border: 1px solid var(--sev-unknown-line); border-left: 4px solid var(--sev-unknown);
  background: var(--sev-unknown-bg); border-radius: var(--radius);
  padding: 12px 14px; margin-bottom: 16px;
}
.cal-truth h2 { font-size: 14px; margin: 0 0 4px; color: var(--sev-unknown); }
.cal-truth p  { font-size: 13px; margin: 0 0 4px; color: var(--ink-2); }
.cal-truth ul { margin: 6px 0 0 18px; font-size: 13px; color: var(--ink-2); }

.cal-settings-list { list-style: none; margin: 0; padding: 0; }
.cal-settings-list li {
  display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: baseline;
  padding: 10px 2px; border-top: 1px solid var(--line-soft);
}
.cal-settings-list li:first-child { border-top: 0; }
.cal-state-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 8px 16px; margin: 4px 0 0;
}
.cal-state-grid label { display: flex; gap: 8px; align-items: flex-start; font-size: 13.5px; }
.cal-state-grid input { margin-top: 3px; }
.cal-weekend-note { font-size: 11.5px; color: var(--muted); display: block; }

/* Generous room at the foot of every calendar screen, per the house rule. */
.cal-foot-space { height: 56px; }

/* Coarse pointers: density yields, 44px is a floor (CR-3). */
@media (pointer: coarse) {
  .cal-cell { min-height: 44px; }
  .cal-chip { padding: 6px 6px 6px 7px; font-size: 12.5px; }
  .cal-more { padding: 8px 0; }
  .cal-daylist > li { padding: 14px 2px; }
}

@media (max-width: 760px) {
  /* The seven-column grid stops being readable long before it stops fitting.
     Below this width the month becomes the same day-by-day list the week and
     agenda views use, which is the shape a partner reads on a phone anyway. */
  .cal-month td { height: auto; min-height: 74px; }
  .cal-month th { font-size: 10.5px; padding: 6px 3px; }
  .cal-cell { padding: 5px 4px; }
  .cal-chip { font-size: 11px; }
  .cal-closed-name { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .cal-cell { transition: none; }
}

@media print {
  .cal-bar, .cal-nav, .toolbar { display: none !important; }
  .cal-month td { height: auto; }
}

/* ===========================================================================
   LAWSERVA - 14 Aug 2026. Appended by hand, like everything below the Tailwind
   build at the top of this file. The build is NOT run: a utility tw.css never
   emitted does not exist and fails silently, so every rule here is explicit
   CSS. Later in the file means later in the cascade, which is what lets these
   override the v2 layer above without editing it.
   ======================================================================== */

/* ---------------------------------------------------------------------------
   1. A DIALOG IS AS TALL AS WHAT IS IN IT.

   `.ls-dialog { height: 93dvh; max-height: none }` gave EVERY dialog in the
   application the same near-full-screen height whatever it held. The worst of
   them is the generated confirmation: one sentence and two buttons, painted at
   93% of the viewport with its actions marooned at the bottom edge, half a
   screen of empty white between the question and the answer. On a 1280x800
   desktop that is a 744px box around about 120px of content.

   height:auto with a max-height is all that was needed. The flex column and
   `.dialog-body { overflow: hidden auto }` above already produce the right
   behaviour underneath a max-height: a short dialog is short, a long one grows
   to the cap and then the BODY scrolls while the header and the footer stay
   put, which is the modal anatomy this project works to.

   Two dialogs keep an explicit height on purpose and are not touched:
     .ls-dialog.record   - it frames a whole document, which has no intrinsic
                           height to size to; height:auto would collapse it.
     .ls-dialog.assistant - a conversation grows a message at a time, and a box
                           that jumps taller on every reply is unreadable.
   --------------------------------------------------------------------------- */
/* Both of these moved to the one MODAL HEIGHT rule at the end of this file. */

/* ---------------------------------------------------------------------------
   2. THE STICKY CHROME.

   This section used to hold three rules reverting the sticky top bar and the
   two sticky alert columns declared earlier in the file. Both halves are gone:
   the rules they reverted have been removed at source, and the reasoning now
   sits once, beside the rules that actually ship. See "THE ALERT COLUMN AND
   THE TOP BAR ARE IN NORMAL FLOW" above. No position:sticky and no
   position:fixed remain anywhere in this file.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   3. THE BOTTOM OF THE PAGE, AND THE NOTCH.

   No fixed chrome remains anywhere in the application after (2), so nothing is
   hidden underneath anything today. The safe-area hook goes in anyway, because
   the house rule requires the clearance to have somewhere to hang, and because
   an iPhone in landscape puts a home indicator across the last 21px of the
   viewport whether or not this application has chrome there.

   Written as `calc(<base> + env(...))` rather than as a separate rule, so a
   later `padding:` shorthand in a media query cannot silently wipe the
   clearance while leaving the base intact - which is the exact failure the
   rule warns about.
   --------------------------------------------------------------------------- */
.page      { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
.site-foot { padding-bottom: calc(34px + env(safe-area-inset-bottom, 0px)); }

@media (max-width: 900px) {
  .page      { padding-bottom: calc(48px + env(safe-area-inset-bottom, 0px)); }
  .site-foot { padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px)); }
}

/* The dialog footer is the bottom edge of a full-height modal on a phone. */
@media (max-width: 640px) {
  .dialog-actions { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
}

/* ---------------------------------------------------------------------------
   4. A CARD HEADING WRAPS.

   .card-head is a flex row with no flex-wrap, and the due diligence card puts
   an h2 plus up to four status pills in it - "Enhanced due diligence required",
   "Sanctions: possible match", the CDD state and now an Edit control. Below
   about 1100px those squashed against each other and the last one overflowed
   the card. The pills are the fastest read on that card and they were the first
   thing to break.

   The heading holds the line and the pills wrap under it, rather than the
   heading itself being squeezed: `min-width: 0` on the h2 alone would have let
   "Customer due diligence" ellipsise, which loses the more important thing.
   --------------------------------------------------------------------------- */
.card-head { flex-wrap: wrap; row-gap: 8px; }
.card-head > h2 { margin-right: auto; }
/* The small count/aside that sits beside a heading. Was an inline
   style="font-size:13px" repeated on six cards. */
.card-head-note { font-size: 13px; }

/* ---------------------------------------------------------------------------
   5. PRINT: A RECORD, NOT A BLANK FORM.

   The print stylesheet hid .side, .topbar, .site-foot, .page-actions, .toolbar
   and .btn, and nothing at all hid the FORMS. So a printed client record
   carried the four empty Add-Party boxes and the whole fourteen-control due
   diligence form as a page of empty rectangles, and the reader had to work out
   which of the boxes on the page were data and which were furniture.

   The forms have since been moved into dialogs, which a closed <dialog> hides
   by itself - so this is the belt to that fix's braces, and it also covers the
   forms in the other agents' screens that have not moved yet. An input is
   never information on paper: whatever it holds is either already stated in
   the record above it or is not part of the record at all.
   --------------------------------------------------------------------------- */
@media print {
  .form-grid,
  .checkline,
  .form-actions,
  .panel-act,
  .say-form,
  .list-busy,
  .dialog-x,
  /* `dialog` here, unqualified, hid the RECORD OVERLAY as well - and an
     earlier print block hides .shell whenever a record overlay is open, on
     the assumption that the overlay itself will print. So with the overlay
     open, the browser's own Print (Ctrl+P) produced a COMPLETELY BLANK PAGE:
     the app hidden by one rule, the overlay hidden by this one, both
     !important, nothing left. The overlay's own Print icon still worked
     because it calls print() on the iframe, which is why this survived.
     Excluding .record restores exactly the case the earlier fix was written
     for, whose comment says it exists so printing does not produce "the list
     underneath and a blank rectangle". */
  dialog:not(.record),
  form input,
  form select,
  form textarea,
  form button { display: none !important; }

  /* A ledger or chronology row must not be split across an A4 page boundary.
     `.card` and `thead` were already handled; the rows were not, so a money
     row could print its description on one page and its amount on the next. */
  tr { break-inside: avoid; page-break-inside: avoid; }

  /* A form that is nothing BUT controls leaves its own border and padding
     behind as an empty box once they are hidden. */
  form:not(.label-page) { border: 0 !important; padding: 0 !important; margin: 0 !important; }

  /* The unit and the time frame are the two things print used to lose with the
     footer, and they are the two a printed page most needs. */
  .unit-caption, .unit-note { display: block !important; color: #000 !important; }
}

/* ---------------------------------------------------------------------------
   6. UNITS AND TIME FRAMES, STATED WHERE THEY ARE READ.

   Both used to live only in .site-foot, which print hides and which the record
   overlay never renders. See money_th()/money_caption()/time_frame_note() in
   lib/form.php.
   --------------------------------------------------------------------------- */
.unit {
  font-weight: 600;
  /* Deliberately not smaller: a currency that has to be squinted at is a
     currency that gets assumed. Same size, lighter, so it reads as the unit
     rather than as part of the column name. */
  color: var(--muted);
}
.unit-caption {
  caption-side: top; text-align: left;
  padding: 0 0 8px; font-size: 12.5px; color: var(--muted);
}
.unit-note {
  margin: 12px 0 0; font-size: 12px; color: var(--muted);
}

/* ---------------------------------------------------------------------------
   7. A LIST THAT IS FETCHING.

   Normal flow, no overlay, nothing fixed. The region is dimmed and a plain
   sentence is put under the toolbar. Dimming is not the whole signal - the
   sentence carries it for anyone who cannot see the dim, and role="status"
   carries it for anyone who cannot see the page.
   --------------------------------------------------------------------------- */
.list-busy {
  margin: -6px 0 14px;
  font-size: 13px;
  color: var(--brand-2);
  font-weight: 600;
}
.is-fetching {
  opacity: .55;
  /* Not pointer-events:none - the rows are still readable and still clickable
     right up until the new page arrives, and taking that away would make a
     slow network feel like a broken screen. */
  transition: opacity .12s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .is-fetching { transition: none; }
}

/* ---------------------------------------------------------------------------
   8. Small pieces the inline styles were carrying.
   --------------------------------------------------------------------------- */

/* A heading INSIDE a .field that names a read-only aside. Not a <label>: a
   label with no control is a heading wearing the wrong element, and a screen
   reader announces it as the name of whatever control it finds next. */
.field-head {
  margin: 0 0 6px;
  font-size: 13px; font-weight: 650; color: var(--ink-2);
}

/* The control that opens a panel's form, where the form used to stand open. */
.panel-act {
  margin: 18px 0 0; padding-top: 16px;
  border-top: 1px solid var(--line-soft);
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
}

/* The standing note at the foot of a list screen. Was style="margin-top:18px"
   on three pages. */
.page-foot-note { margin-top: 18px; }

/* A validation message this browser produced rather than the server. Same
   anatomy as the server-rendered .err above it, deliberately: a person should
   never have to learn two shapes for "this field is wrong". */
.err.is-live { margin-top: 5px; }

/* The frame note at the foot of a record read inside the overlay. Inside the
   scrolling frame, in normal flow, below the last card - it is the last thing
   on the record, not chrome laid over it. */
.modal-frame-note { padding: 0 28px 28px; margin-top: 0; }
@media (max-width: 900px) { .modal-frame-note { padding: 0 16px 24px; } }

/* ===========================================================================
   LAWSERVA DESIGN SYSTEM v3 - OPERATE POLISH, 16 AUG 2026

   A quiet refresh on top of the existing system. The direction stays the
   same: instrument-grade, calm, high-signal, designed for a person working
   inside the application for a full day. The changes below are micro-layout,
   surface depth, and light/dark resilience, not a new visual world.
   ------------------------------------------------------------------------ */

html {
  color-scheme: light;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection {
  background: var(--brand-tint);
  color: var(--brand-ink);
}

body {
  background:
    radial-gradient(1200px 600px at 88% -10%, rgb(27 84 136 / .045), transparent 60%),
    var(--bg);
}

/* ---------------------------------------------------------------------------
   THE SIGN-IN SCREEN MUST KEEP ITS DARK PLANE.

   The build on line 2 sets `body.auth{background:#0d2941 url(login-bg.jpg)...}`
   and colours the wordmark, the strapline and the password-help line white on
   top of it. That is the design and it is correct. But that rule lives inside
   Tailwind's @layer, and the plain `body` rule immediately above is hand
   written and UNLAYERED - and an unlayered rule beats a layered one at any
   specificity, however much less specific it is. So `body` won, took the navy
   and the photograph with it, and left white text on var(--bg): measured at
   about 1.06:1 where AA wants 4.5:1. The product's own name was invisible on
   the first screen anybody sees.

   It also hid from the 19 Aug sweep, which ran with the OS in dark: the dark
   block then at the end of this file re-coloured these three elements, so the
   screen measured clean in the one theme nobody was shipping it in. That block
   has since been removed with the rest of the dark theme (Alex, 24 August
   2026), so this rule is now the only thing holding the sign-in screen up.

   Restating it here, unlayered and after `body`, is what makes it stick. Do NOT
   "fix" this by re-running the Tailwind build: this file is hand edited after
   the build and a rebuild discards everything below line 2.
   ------------------------------------------------------------------------ */
body.auth {
  background:
    linear-gradient(rgb(13 41 65 / .72), rgb(10 32 52 / .82)),
    url("login-bg.jpg") 50% / cover no-repeat,
    #0d2941;
  min-height: 100dvh;
}

/* Keep the shell calm and give the sidebar a slightly richer plane than the
   flat navy it replaced. */
.shell {
  background: var(--bg);
}

/* The sidebar's own plane, and the scroll shadows that say it scrolls.

   THE SHADOWS ARE NOT DECORATION. .shell is 100dvh with overflow hidden, so on
   a 1280x800 laptop 21 of the 38 destinations sit below the fold - Time,
   Billing, Client Account, Bank Reconciliation, the Accountant's Report and the
   Suspicious Transactions register among them. macOS paints no scrollbar until
   the pointer is already over the column, so without these there is nothing at
   all to say there is more.

   They were written once already, further up this file, and then deleted by
   accident: this rule used the `background` SHORTHAND, which resets
   background-image, -position, -repeat, -size and -attachment before assigning,
   so it wiped the four gradients and the `local` attachment they depend on.
   Measured after that: 2 gradients, `scroll, scroll`. Everything the shorthand
   used to set is now set here explicitly, in one place, so the two rules can no
   longer fight. background-attachment `local` is what makes a shadow appear
   only at the end that actually has more content. */
.side {
  background-color: #0d2941;
  background-image:
    linear-gradient(var(--brand-ink), rgb(13 41 65 / 0)),
    linear-gradient(rgb(13 41 65 / 0), var(--brand-ink)),
    linear-gradient(rgb(255 255 255 / .22), rgb(255 255 255 / 0)),
    linear-gradient(rgb(255 255 255 / 0), rgb(255 255 255 / .22)),
    radial-gradient(280px 160px at 12% 0%, rgb(255 255 255 / .08), transparent 70%),
    linear-gradient(180deg, #0d2941 0%, #0a2034 100%);
  background-position: 0 0, 0 100%, 0 0, 0 100%, 0 0, 0 0;
  background-repeat: no-repeat;
  background-size: 100% 22px, 100% 22px, 100% 7px, 100% 7px, auto, auto;
  background-attachment: local, local, scroll, scroll, scroll, scroll;
  box-shadow: inset -1px 0 0 rgb(255 255 255 / .04);
}

/* Below 900px the drawer is in normal flow and does not scroll on its own, so
   the shadows would be painted permanently over the first and last item. */
@media (max-width: 900px) {
  .side {
    background-image:
      radial-gradient(280px 160px at 12% 0%, rgb(255 255 255 / .08), transparent 70%),
      linear-gradient(180deg, #0d2941 0%, #0a2034 100%);
    background-size: auto, auto;
    background-attachment: scroll, scroll;
  }
}

.brand {
  min-height: 44px;
  padding: 6px 8px;
}

.brand-mark {
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .12), 0 8px 20px rgb(0 0 0 / .22);
}

.sidenav a {
  padding: 9px 11px;
  gap: 11px;
  margin-bottom: 1px;
  transition: background-color var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}

.sidenav a:hover {
  transform: translateX(1px);
}

.navgroup {
  padding-bottom: 4px;
  margin-bottom: 7px;
  border-bottom: 1px solid rgb(255 255 255 / .08);
}

.sidefoot {
  margin-top: 4px;
}

.topbar {
  min-height: 58px;
  padding: 9px 24px;
  gap: 14px;
  box-shadow: 0 1px 0 rgb(20 28 40 / .04);
}

.gsearch {
  gap: 8px;
}

.gsearch input {
  border-radius: var(--r-sm);
  /* 34px on the left, NOT 12px. The magnifier is painted as a background at
     `no-repeat 10px center` and is 16px wide, so it occupies x=10 to x=26.
     This rule is the sixth definition of .gsearch input in this file and it
     dropped the reservation the first one made while leaving the icon in
     place, so the glyph sat on the first letter of the placeholder on every
     authenticated screen. */
  padding: 9px 12px 9px 34px;
  font-size: 13.5px;
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              background-color var(--t-fast) var(--ease-out);
}

.gsearch input:focus {
  box-shadow: var(--focus);
}

.who {
  gap: 8px;
}

.page {
  /* Longhand, deliberately. This was `padding: 22px 24px 52px`, a shorthand
     252 lines BELOW the calc(56px + env(safe-area-inset-bottom)) that the
     comment above that rule says was written specifically so a later shorthand
     could not wipe it. It wiped it: measured padding-bottom 52px flat, with
     the env() term gone from the cascade. Same specificity, later in source.
     Split into longhands so the bottom is set in exactly one place. */
  padding-top: 22px;
  padding-left: 24px;
  padding-right: 24px;
}

.page-head {
  margin-bottom: 18px;
  gap: 14px;
}

.page-actions {
  gap: 8px;
}

.card {
  border-radius: var(--r-md);
  box-shadow: var(--e-1);
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}

.card.flush {
  padding: 0;
  overflow: hidden;
}

.card-head {
  min-height: 28px;
}

.btn {
  box-shadow: 0 1px 2px rgb(20 28 40 / .08);
}

.btn:not(.secondary):not(.ghost):not(.danger) {
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .08), 0 1px 2px rgb(20 28 40 / .12);
}

.btn.secondary {
  box-shadow: 0 1px 2px rgb(20 28 40 / .05);
}

.btn:active:not(:disabled) {
  transform: translateY(1px) scale(.982);
}

.toolbar {
  box-shadow: 0 1px 0 rgb(20 28 40 / .03);
}

.subnav a {
  transition: background-color var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}

.subnav a:focus-visible {
  outline-offset: 2px;
}

.table-scroll {
  scrollbar-width: thin;
  scrollbar-color: #b8c4d2 transparent;
}

.table-scroll::-webkit-scrollbar {
  height: 10px;
}

.table-scroll::-webkit-scrollbar-thumb {
  background: #b8c4d2;
  border: 2px solid transparent;
  background-clip: padding-box;
  border-radius: 999px;
}

.table-scroll::-webkit-scrollbar-thumb:hover {
  background: #94a5b6;
  border: 2px solid transparent;
  background-clip: padding-box;
}

.empty {
  border-radius: var(--r-md);
}

.dialog-actions {
  background: var(--surface-sunk);
  border-top: 1px solid var(--line);
  padding: 14px 22px;
}

.dialog-x {
  transition: background-color var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}

/* 44 SQUARE ON A COARSE POINTER. The base rule sets 36x36; the max-width:900px
   button rule then stretched only the HEIGHT to 44px, so the close control on
   EVERY modal in the application measured 36x44 - under the floor on the one
   axis nobody checked. Its sibling .rec-tool was given exactly this treatment
   explicitly; the X was missed. */
@media (pointer: coarse), (max-width: 900px) {
  .dialog-x { width: 44px; height: 44px; }
}

.ls-dialog {
  overflow: hidden;
}

/* ---------------------------------------------------------------------------
   Dark theme. Light remains the default. A person using a dark operating
   system gets a dark workspace without an extra control to find, while all
   the semantic tokens stay exactly where they are.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .sidenav a:hover {
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
   Mobile touch targets. The previous rules were gated on coarse pointers
   only, which missed narrow desktop windows and automated mobile checks.
   These are the controls that must be at least 44px tall on a small screen.
   ------------------------------------------------------------------------ */
@media (max-width: 700px) {
  /* .metric is NOT in this list. It is a multi-child CARD declared
     display:block above; `display:inline-flex` here turned the Diary's four
     counters and both client_view.php blocks into horizontal flex rows on
     every viewport under 700px - label, figure and the whole explanatory
     sentence side by side in a 159px column, 198px tall, unreadable. It was
     already far taller than 44px and never needed a floor. Every other
     selector below is a genuine link or button and is correctly served. */
  .skip,
  .brand,
  .usermenu a,
  .subnav a,
  .tabs a,
  .pager a,
  .attn-a a,
  .alert-act a,
  .page-aside a:not(.navicon),
  .row-actions .btn,
  .page-actions .btn {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .alert-act a,
  .attn-a a,
  .page-aside a:not(.navicon) {
    padding-top: 10px;
    padding-bottom: 10px;
  }

  input[type="text"],
  input[type="email"],
  input[type="password"],
  input[type="date"],
  input[type="time"],
  input[type="tel"],
  input[type="url"],
  input[type="number"],
  input[type="search"],
  select,
  textarea {
    min-height: 44px;
    font-size: 16px;
  }

  /* A TAP TARGET, NOT EVERY LINK THAT HAPPENS TO SIT IN A CELL.
     .cal-cell is the whole calendar day - a block region holding a date and a
     stack of chips - and inline-flex made it shrink-to-fit, so its
     white-space:nowrap chips stretched it to 573px inside a 375px phone and
     76px of the month view sat off the screen with nothing able to scroll to
     it. This selector (0,2,3) was beating .cal-cell{display:block} (0,1,0).
     Measured and fixed 29 Aug 2026. */
  tbody td a:not(.cal-cell),
  .tbl-stack.is-stacked td:first-child a:not(.cal-cell) {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* ===========================================================================
   LAWSERVA - 19 Aug 2026. Appended by hand, below everything above it, for the
   same reason the 14 Aug section says: the Tailwind build is NOT re-run, later
   in the file is later in the cascade, and that is what lets these fix rules
   further up without editing them.
   ======================================================================== */

/* ---------------------------------------------------------------------------
   PRINTING THE RECORD OVERLAY IS STILL CLIPPED TO ONE PAGE.

   The print block at line 175 already hides the shell and sets the record
   dialog to `height:auto`, and its comment says the blank-page case was fixed.
   It was - from blank to ONE page. Measured here on 19 Aug 2026 with the
   overlay open on matter 24 under print media, before this rule existed:

     dialog computed height 1044.38px, max-height 1044.39px, overflow hidden
     iframe 1044px, the framed record's own document 5197px
     Page.printToPDF at A4 -> Pages: 1, ending mid-card

   Two later unlayered rules were undoing the print block: `.ls-dialog` at the
   "A DIALOG IS AS TALL AS WHAT IS IN IT" section sets `max-height:93dvh`, and
   the shadow section sets `overflow:hidden`. Both have the same specificity as
   `.ls-dialog.record` in the print block but come after it, so both won.

   Resetting them is NECESSARY and is NOT SUFFICIENT, and saying so here is the
   point of this comment. An <iframe> is a replaced element: it does not grow to
   its document, so with these three declarations alone the frame still computes
   1123px and the PDF is still one page - measured, not assumed. What produces a
   correct file is these declarations PLUS the frame being given its content's
   height for the duration of the print. The second half lives in assets/app.js
   and is a `beforeprint` / `afterprint` pair on the overlay:

     beforeprint -> frame.style.height = frame.contentDocument
                      .documentElement.scrollHeight + 'px'
     afterprint  -> frame.style.height = ''

   Proved in the sandbox by injecting exactly those two halves: 5 pages, the
   same count the overlay's own Print icon produces by calling print() on the
   frame. Either half on its own gives 1 page.

   `!important` rather than order alone, because this is a correctness rule on a
   document that leaves the building, and the next hand-edit appended below must
   not be able to silently re-clip it.
   --------------------------------------------------------------------------- */
@media print {
  /* `[open]` is load bearing. The overlay is built once and reused, so a closed
     `dialog.record` is sitting in the document on most screens, hidden only by
     the user-agent's `dialog:not([open]){display:none}`. An unqualified
     `display:block` here would have printed an empty overlay on top of every
     ordinary page in the application. */
  .ls-dialog.record[open],
  dialog.ls-dialog.record[open] {
    max-height: none !important;
    height: auto !important;
    overflow: visible !important;
    /* `display:block` is the declaration that actually releases the frame. The
       dialog is a flex column and `.rec-frame` is `flex:auto`, i.e. shrink 1,
       so inside a flex box the frame is squeezed back to the dialog's height
       however tall its own document is. Measured: with the resets above but
       still flex, the frame computed 1123px and the PDF was 1 page; with
       `display:block` as well, the frame computed 5197px and the PDF was 5
       pages. The header and the footer are already `display:none` in print, so
       nothing else in this dialog is relying on the flex column. */
    display: block !important;
    position: static !important;
  }

  /* Sized by script for the print (see above). Without a height of its own the
     frame takes the dialog's, which is why it must not be pinned here.

     19 Aug 2026, second pass. The comment above this block reports that
     `display:block` took the frame to 5197px. It did not, and reading it as
     evidence is the trap this codebase keeps setting: that number was measured
     with the app.js half INJECTED. The app.js half was never written - grep for
     `beforeprint` in assets/app.js returns nothing - so what shipped is the CSS
     half alone, and the CSS half alone made the case WORSE, not better.

     Measured here on matter 24 with the overlay open, print media emulated,
     Page.printToPDF at A4: the frame computed 150px, not 1044px and not
     5197px. 150px is the HTML default height of an <iframe> that is no longer
     a flex item and has been told `min-height:0` at line 132. The printed sheet
     carried the matter title, the number chip, the meta line and a sliced
     "File" heading, and the remaining four fifths of the page was white. Before
     the block above existed the same print gave a full page of the record; the
     release to `display:block` traded a full page for a sixth of one.

     `min-height`, not `height`. A minimum floors the frame at one printable
     page whatever the layout does, and it does NOT cap it, so when the app.js
     `beforeprint` pair is written and sets an inline pixel height taller than a
     page, the taller value still wins and this rule quietly stops mattering.
     A `height: 100vh !important` here would have overridden that inline style
     and pinned the fix at one page forever.

     The value is `100vh` less the height the notice below occupies. At a flat
     `100vh` the frame no longer fitted on the sheet under its own notice, and
     Chrome moved the whole replaced box to page two, giving a first page that
     was the notice and nothing else. Measured at A4 with 10mm margins: notice
     plus rule plus margin is 76px, and `calc(100vh - 84px)` puts the frame back
     on page one with the record starting immediately under the notice. */
  .ls-dialog.record .rec-frame {
    max-height: none !important;
    min-height: calc(100vh - 84px) !important;
  }

  /* A ONE PAGE EXTRACT MUST SAY IT IS AN EXTRACT.

     CSS cannot paginate an iframe: a replaced element prints the box it
     occupies in the parent document and nothing more, so the whole file needs
     the frame given its content's height before the print starts, which is
     script and lives in a file this agent does not own. What CSS can do is stop
     the sheet asserting it is the complete record, which is the half of the
     defect that actually misleads. A matter file handed to counsel that stops
     mid-card with nothing marking the cut is the error the file copy exists to
     prevent.

     The `:has()` test is what makes this line RETIRE ITSELF. `beforeprint` sets
     the frame's height as an inline style, so the moment the app.js half is
     written the frame carries `style="height:...px"`, this selector stops
     matching, the sheet paginates in full and the notice disappears with no
     further edit here. Written as a `::before` on the dialog rather than on the
     frame because generated content on a replaced element is not rendered.

     19 Aug 2026, verification pass. The first wording of this notice read
     "Extract only. This sheet is the first page of the record. Use the Print
     control in the record header to print the complete file." That asserts a
     cut, and CSS has no way to know whether one happened: the selector cannot
     see the framed document's height, so the notice prints on EVERY overlay
     print, including one where the whole record fitted. Measured here, overlay
     open and the frame navigated to a short record (the "Not found" record
     page, which is what a stale or cross-firm link opens): frame 816px, the
     framed document 816px with a 287px body, i.e. nothing whatever was cut,
     and the sheet still said the rest of the file was missing. A notice that
     over-reports is the same class of truth defect as a cut that goes
     unreported, and on an accounts or a client file it teaches the reader to
     disbelieve the notice on the day it is true.

     So the wording now DESCRIBES THE SHEET instead of asserting what is absent:
     it says the sheet carries the first page and nothing beyond it, which is
     true whether the record ran to six pages or to one, and it still points at
     the control that prints the whole file. */
  .ls-dialog.record[open]:has(.rec-frame:not([style*="height"]))::before {
    content: "This sheet carries the first page of the record and nothing beyond it. Use the Print control in the record header for the complete file.";
    display: block !important;
    margin: 0 0 10px !important;
    padding: 0 0 6px !important;
    border-bottom: 1px solid #000 !important;
    font-size: 10pt !important;
    font-weight: 700 !important;
    color: #000 !important;
  }

  /* The modal backdrop printed as a grey wash behind the record. On screen it
     is what says "the list is still there underneath"; on paper there is no
     underneath, and a grey ground costs toner and legibility. */
  .ls-dialog::backdrop {
    background: none !important;
  }
}

/* ---------------------------------------------------------------------------
   A TABLE THAT SCROLLS SIDEWAYS NOW SAYS SO.

   Measured on 19 Aug 2026 at 375px, signed in as the managing partner: on
   reports.php the utilisation table is 759px inside a 293px `.table-scroll`
   and hides four columns - Non-billable, Target for the period, Utilisation
   and Value recorded - and the review table hides Review due. The scroller
   computed `background-image: none`, `mask-image: none`, `box-shadow: none`,
   and `offsetHeight - clientHeight` was 0, i.e. the scrollbar is an OVERLAY
   scrollbar that occupies no space and is invisible at rest. macOS and iOS
   both behave that way, so the `scrollbar-color` and `::-webkit-scrollbar`
   rules further up buy nothing until the reader is already dragging. Read in
   the rendered pixels: a figure sliced vertically at the card border with
   nothing at all to mark the cut.

   The technique is the one the sidebar already uses and its comment already
   explains: two COVER layers painted in the surface colour and attached
   `local` so they travel with the content, and two SHADOW layers attached
   `scroll` so they stay pinned to the box. At rest each cover sits exactly on
   top of its shadow and nothing shows; scroll away from an edge and that
   edge's cover slides off, revealing the shadow underneath. No script, no
   extra element, and a table that fits shows nothing - which is why this is
   safe to put on all 40-odd tables at once.

   This is the AFFORDANCE half of the defect only, and it is worth being plain
   about that. The other half is that reports.php and billing.php tables are
   excluded from stacking into labelled blocks on a phone by
   `assets/app.js: eligible()`, which tests `t.classList.contains('ledger')` -
   a class naming a table's LOOK, worn by 22 tables, and not the running
   balance the comment beside it cites. That file is not this file's to change.
   --------------------------------------------------------------------------- */
:root {
  /* The edge shade for scroll shadows. A token, not a literal, because it has
     to invert with the theme and because the same value will be wanted by the
     next scrolling surface. */
  --edge-shade: rgb(20 28 40 / .20);
}

.table-scroll {
  background-image:
    linear-gradient(to right, var(--card) 60%, rgb(255 255 255 / 0)),
    linear-gradient(to left,  var(--card) 60%, rgb(255 255 255 / 0)),
    linear-gradient(to right, var(--edge-shade), rgb(255 255 255 / 0)),
    linear-gradient(to left,  var(--edge-shade), rgb(255 255 255 / 0));
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-repeat: no-repeat;
  background-size: 26px 100%, 26px 100%, 11px 100%, 11px 100%;
  background-attachment: local, local, scroll, scroll;
}

/* Stacked into labelled blocks, so there is nothing to scroll and the shadows
   would be painted permanently down the first and last column of every block.
   The same guard the sidebar carries for its drawer state. */
.table-scroll:has(> table.is-stacked) {
  background-image: none;
}


@media print {
  /* Paper does not scroll, and the print block further up already sets
     `.table-scroll { overflow: visible }`, so a cover and a shadow at each
     edge would be two grey bands down a printed report for no reason. */
  .table-scroll {
    background-image: none !important;
  }
}

/* ---------------------------------------------------------------------------
   A PRINTED SHEET NOW SAYS WHOSE BOOKS IT IS, AND WHICH VIEW IT IS.

   Measured on 19 Aug 2026, ledger.php?tab=office rendered under the print
   stylesheet as the accounts executive: `.side`, `.topbar` and `.site-foot`
   all computed display:none, and nothing replaced any of them. So the sheet
   carried the heading, the tab strip and the rows, and nowhere on it was the
   firm's name. An extract from the cash book with no firm on its face is not a
   document to put in front of a reporting accountant or a Bar Council
   inspector, and it is Gate C as well: which tenant this is must be legible
   without clicking, and paper has no clicking.

   The firm's name is already in the markup, in `.topfirm` inside `.topbar`,
   where lib/layout.php puts it and where CSS shows it below 900px only. So
   print does not need new markup: it needs the topbar back as a one-line
   identification strip carrying that and nothing else. The screen-reader-only
   "Firm: " label is un-hidden with it, because on paper the words are what
   make the line mean something.

   The tab strip is the second half. Four links to views the reader cannot
   follow are furniture on paper - but deleting the strip outright would take
   with it the ONLY thing on the sheet that says which of the five ledger views
   was printed, since ledger.php heads all five "Client account". So the strip
   collapses to the current tab, drawn as a plain label. An office cash book
   then prints with the firm's name and the words "Office cash book" on it,
   whatever the H1 above says.

   WHY THIS BLOCK IS INSIDE `@layer components` WHEN NOTHING ELSE DOWN HERE IS.
   This is the trap at the top of this file read the other way round, and it
   cost an hour. The note beside `.money` in the report print block says an
   UNLAYERED rule beats a LAYERED one whatever its weight - true, and true only
   for NORMAL declarations. For `!important` declarations the cascade inverts:
   a layered important beats an unlayered important, and layer order is
   compared BEFORE specificity, so no selector written out here could ever have
   won. The rule doing the hiding is
   `.no-print,.side,.topbar,.site-foot{display:none!important}` inside
   `@layer components` from the build. Measured: with the same declaration
   written unlayered at the end of this file, and again injected into a fresh
   <style> after everything, `.topbar` still computed `none`; only an inline
   style moved it. Re-opening `components` here appends to that layer, and
   within one layer the later rule wins, which is what makes this work.

   WHAT THIS DOES NOT FIX, because both need a file this agent does not own:
     - ledger.php:155 `page_title('Client account', ...)` is unconditional
       across all five tabs, so an office extract is still headed with the name
       of the client account.
     - There is still no date of printing and no "as at" date anywhere on the
       sheet. Neither exists in the DOM, and CSS cannot invent one; it wants a
       print-only line from lib/layout.php.
   --------------------------------------------------------------------------- */
@layer components {
  @media print {
    .topbar {
      display: block !important;
      min-height: 0 !important;
      margin: 0 0 14px !important;
      padding: 0 0 8px !important;
      background: none !important;
      border-bottom: 1px solid #ccc !important;
      box-shadow: none !important;
    }

    /* Search, the assistant, the account menu and the feedback control are all
       controls, and a control on paper is furniture. */
    .topbar > * {
      display: none !important;
    }

    .topbar > .topfirm {
      display: block !important;
      margin: 0 !important;
      max-width: none !important;
      overflow: visible !important;
      white-space: normal !important;
      text-overflow: clip !important;
      font-size: 11pt !important;
      font-weight: 700 !important;
      color: #000 !important;
    }

    /* On screen this reads "Firm: " to a screen reader only. On paper it is the
       difference between a name floating at the top of a page and a line that
       says what the name IS. */
    .topbar .topfirm .sr-only {
      position: static !important;
      width: auto !important;
      height: auto !important;
      margin: 0 !important;
      overflow: visible !important;
      clip-path: none !important;
      white-space: normal !important;
    }

    .tabs {
      border-bottom: 0 !important;
      margin-bottom: 12px !important;
    }

    .tabs a:not([aria-current="page"]) {
      display: none !important;
    }

    .tabs a[aria-current="page"] {
      padding-left: 0 !important;
      border-bottom: 0 !important;
      color: #000 !important;
      font-weight: 650 !important;
    }

    /* AN OFFICE CASH BOOK NO LONGER PRINTS UNDER THE WORDS "CLIENT ACCOUNT".
       ------------------------------------------------------------------
       Second pass, 19 Aug 2026. The block above put the firm's name and the
       current view on the sheet and then said in writing what it could not
       reach: ledger.php:155 heads all five ledger views `page_title('Client
       account', 'Client money and office money, kept separate.')`, so an
       extract of the OFFICE cash book still printed with the name of the
       CLIENT account across the top of it. Re-measured here before this rule:
       ledger.php?tab=office rendered under the print stylesheet as the accounts
       executive gives H1 "Client account", the subtitle beneath it, and then
       rows headed "Office cash book".

       Keeping client money and office money visibly apart is the whole of the
       discipline the Solicitors' Accounts Rules impose, and the page's own
       subtitle says so. A sheet that says "Client account" on its face and
       carries office transactions misdescribes the record - in the accounts
       file, and worse in an Accountant's Report pack.

       WHY THIS IS A DELETION AND NOT A RELABELLING. CSS could paint a
       replacement title with generated content, and that would be a second
       place naming the view, drifting from ledger.php the first time anybody
       edits either. The sheet already names itself twice without help: the
       collapsed tab label directly above the rows, and the table's own <h2>.
       Removing a false line is honest; inventing a true one in a stylesheet is
       not this file's job.

       Scoped to the office tab alone. "Client account" is the correct heading
       for the client cash book, the client balances and the unpresented
       cheques, and the office tab is the one view of the five that is not
       client money. The hook is the tab strip's own `aria-current`, which is
       the only thing in the DOM that says which view is open; if ledger.php
       ever stops marking it the rule simply stops matching and the sheet is no
       worse than it is today.

       The client-account BALANCE CARD goes with it, for the same reason and no
       further: a balance is a statement of an account's position and belongs on
       that account's own extract, so RM 250,000.00 of client money has no place
       on a sheet of the firm's own income. Nothing else is touched. The
       client-money integrity check and the reconciliation alerts stay on the
       paper deliberately - they are warnings, they name the account they are
       about in their own first line, and taking a warning off a printed
       accounts record would be removing a control rather than fixing one. */
    body:has(.subnav a[href*="tab=office"][aria-current="page"]) .page-head h1,
    body:has(.subnav a[href*="tab=office"][aria-current="page"]) .page-head .sub,
    body:has(.subnav a[href*="tab=office"][aria-current="page"]) .acct.client {
      display: none !important;
    }

    /* The two account cards sit in a two-column grid. With the client card
       gone the office card would keep half the width and print beside an empty
       column, which reads as a card that failed to render rather than as one
       that was never there. */
    body:has(.subnav a[href*="tab=office"][aria-current="page"]) .grid-2:has(> .acct.client) {
      grid-template-columns: minmax(0, 1fr) !important;
    }
  }
}

/* ---------------------------------------------------------------------------
   A SCREEN-READER-ONLY LABEL WAS DRAGGING THE WHOLE PAGE SIDEWAYS.

   time.php is the one screen in this application whose page body scrolls
   horizontally, and it is the screen a fee earner uses most on a phone.
   Measured at 375px on 19 Aug 2026, signed in as the managing partner:
   documentElement.scrollWidth 632 against a clientWidth of 375, while
   body.scrollWidth was 375. Isolated in the same probe by deleting the two
   `.sr-only` nodes on the page: scrollWidth went straight back to 375.

   The offender is `<th scope="col"><span class="sr-only">Actions</span></th>`
   in the 720px-wide time table. `.sr-only` is `position: absolute`, and with
   no positioned ancestor between it and the initial containing block it is
   laid out at its static position - x = 631, inside a table clipped to a 293px
   `.table-scroll` - but it is NOT clipped by that scroller, because a scroller
   only clips boxes whose containing block is inside it. So a 1px by 1px box
   nobody can see added 257px of pannable emptiness to the right of the app.

   `position: relative` here makes the scroller the containing block, which is
   the actual cause rather than the symptom, and it protects every other
   sr-only label and `<caption class="visually-hidden">` in a wide table -
   ledger_recon.php's proof table and calendar.php's month grid both have one -
   including ones not written yet. Checked across ten screens before changing
   it: the ONLY absolutely positioned descendants of any `.table-scroll` in
   this application are those hidden labels, so nothing is relying on the old
   containing block.
   --------------------------------------------------------------------------- */
.table-scroll {
  position: relative;
}

/* ---------------------------------------------------------------------------
   "CORRECT THE 2 FIELDS MARKED BELOW" NOW POINTS AT SOMETHING THAT IS MARKED.

   The build carries `.field.has-error input,.field.has-error select,
   .field.has-error textarea{border-color:var(--bad);background-color:snow}`
   and, later in the same layer, `.ls-dialog input[type=text]{...;border:1px
   solid var(--line);...}`. Both weigh (0,2,1); the second is later, and the
   `border` SHORTHAND resets border-color before assigning, so it won. Every
   form in this application lives in a dialog by house rule, so in practice the
   error state was never drawn at all.

   Measured on 19 Aug 2026, the Record Money dialog on ledger.php submitted as
   the managing partner with amount "12,50O.00" and an empty description: the
   dialog came back with `class="field has-error"` on #f_amount and
   #f_description, and both computed border-top-color rgb(221,227,234) - the
   same --line grey, byte for byte, as #f_cheque_no and #f_reference, which had
   no error at all. --bad (#983030) was never applied to anything. The banner
   above asserted a visual state the page did not render, on the client-money
   posting form.

   THREE THINGS ARE DIFFERENT HERE.

   1. The rule is UNLAYERED and at the end of the file, so it beats the layered
      build whatever that does next. This is the trap the `.money` note further
      up records: an unlayered normal declaration beats a layered one whatever
      its weight.
   2. The border also gets WIDER. Colour alone is not an acceptable channel for
      state anywhere else in this product and it is not one here either - a
      clerk who cannot separate a dark red edge from a grey one still sees a
      2px edge among 1px ones. `box-sizing: border-box` is set globally, so the
      extra pixel comes out of the padding and nothing moves on the page.
   3. `background-color: snow` is gone. It is a hard-coded near-white that
      would have rendered as a white slab in a dark dialog the moment this rule
      started winning. --bad-bg is the token for exactly this and carries a
      value in each theme.
   --------------------------------------------------------------------------- */
.field.has-error input,
.field.has-error select,
.field.has-error textarea,
.ls-dialog .field.has-error input,
.ls-dialog .field.has-error select,
.ls-dialog .field.has-error textarea {
  border-color: var(--bad);
  border-width: 2px;
  background-color: var(--bad-bg);
}

/* ---------------------------------------------------------------------------
   A FIELD IS NOW VISIBLE AS A FIELD. WCAG 2.1 SC 1.4.11.

   Every text input, select and date control in the application was bounded by
   a single 1px --line edge, and the field's fill is the same colour as the
   surface behind it, so that edge was the ONLY thing saying where the control
   is. Measured 19 Aug 2026 from the shipped tokens, and again against the
   rendered pixels inside the Record Money dialog:

     light  #dde3ea on #ffffff = 1.29:1   (on --bg #f4f6f8 = 1.19:1)
     dark   #273647 on #151f2c = 1.35:1   (on --bg #0e1620 = 1.48:1)

   SC 1.4.11 Non-text Contrast requires 3:1 for the visual information needed
   to identify a control. This is the one accessibility failure in the product
   that hits the person the brief names - the clerk with poor eyesight - and it
   hits hardest on the client-money posting form, where "which of these fifteen
   boxes is still empty" has to be answerable at a glance before Post Entry
   writes a permanent, unamendable entry.

   A NEW TOKEN RATHER THAN A DARKER --line. --line also draws card edges, table
   rules and dividers. Those are decoration between two areas of content, not
   control boundaries; 3:1 is not required of them and would make every table
   in the product look like a spreadsheet grid. So --field-line is introduced
   and used ONLY where a control's boundary is drawn, and --line is left alone.

   Values chosen with margin over the 3:1 floor against every surface a field
   actually sits on:

     light #7a8492 - 3.79:1 on --card #ffffff, 3.50:1 on --bg #f4f6f8,
                     3.65:1 on --s2 #f7f9fb
     dark  #6f7f92 - 4.06:1 on --card #151f2c, 4.44:1 on --bg #0e1620,
                     3.70:1 on --s3 #1a2735

   THE FOCUS RULE IS RESTATED BELOW AND MUST STAY THERE. The build's
   `input:focus,select:focus,textarea:focus{border-color:var(--brand-2)}` is
   inside `@layer components`; an UNLAYERED normal declaration beats a layered
   one whatever its weight, so the resting rule here would have silently
   outranked the focus rule and a focused field would have kept the resting
   grey. Restating focus unlayered, after it, is what keeps the focus border
   the brand colour. The error state is not restated because it does not need
   to be: `.field.has-error input` weighs (0,2,1) against this rule's (0,1,1)
   and wins on specificity within the same unlayered context, so a rejected
   field stays red, focused or not, which is the correct behaviour anyway.
   --------------------------------------------------------------------------- */
:root {
  --field-line: #7a8492;
}


input[type="text"],
input[type="email"],
input[type="password"],
input[type="date"],
input[type="time"],
input[type="tel"],
input[type="url"],
input[type="number"],
input[type="search"],
input[type="file"],
select,
textarea,
.gsearch input,
.composer {
  border-color: var(--field-line);
}

input:focus,
select:focus,
textarea:focus,
.gsearch input:focus,
.composer:focus-within {
  border-color: var(--brand-2);
}

/* ---------------------------------------------------------------------------
   THE MONTH DIARY STOPS CUTTING SUNDAY OFF.

   `.table-scroll table:has(thead th:nth-child(6)){min-width:720px}` is a floor
   written for six-column LIST tables, where each column holds a sentence. The
   month grid has seven columns holding a date numeral and a chip, and it
   inherited the floor because it happens to have a sixth `th`. Measured on
   19 Aug 2026 at nine widths, signed in as the managing partner, reading the
   overflow of `.cal-month` past the right edge of its scroller:

     759  -> 0    (below 760 the grid becomes the day list)
     768  -> 49   Sunday clipped
     800  -> 17   Sunday clipped
     900  -> 0    (the sidebar is hidden, so the content column is 803px)
     901  -> 160  Saturday AND Sunday clipped, the 236px sidebar is back
     960  -> 101  Sunday clipped
     1024 -> 37   Sunday clipped
     1080 -> 0
     1280 -> 0

   1024x768 is iPad landscape and 900 to 1080 is any window snapped to half a
   1920 screen, which is how the diary is read at a mention. A whole weekend
   column disappearing with no mark is how a Saturday completion, an
   undertaking expiry or a stamping deadline that falls on a weekend gets
   missed, and the legend below the grid goes on explaining a shaded
   non-working day the reader cannot see.

   `.cal-month` is `table-layout: fixed; width: 100%`, so with the floor
   released the seven columns simply divide whatever the card gives them. The
   worst case in the whole band is 901px, where the scroller is 560px and the
   columns come out at 80px each - enough for the date and a truncated chip,
   and the cells already shrink their type below 760px for the same reason.

   NOT DONE BY WIDENING THE DAY-LIST FALLBACK, which was the other candidate.
   The list shape below 760px is CSS, but the ARIA roles that make a stacked
   table readable are re-asserted by assets/app.js against its own hard-coded
   `matchMedia('(max-width: 760px)')`. Stacking at a wider breakpoint from CSS
   alone would have produced a run of unlabelled text for a screen reader
   between 761px and 1079px - a worse defect than the one being fixed, in a
   file this agent does not own.

   The specificity is the house idiom from the `.page-aside` rule further up:
   :has() carries the weight of its argument, so the floor computes (0,2,3) and
   has to be beaten on its own terms rather than by document order.
   --------------------------------------------------------------------------- */
.table-scroll table.cal-month:has(thead th:nth-child(6)) {
  min-width: 0;
}

/* ---------------------------------------------------------------------------
   THE RECORD OVERLAY ON A PHONE: NAME THE FILE, AND HIT THE 44px FLOOR.

   Measured 19 Aug 2026 at 375x812, matter 24 opened from the Matters list:
   dialog 339 x 744, header bar 69, footer 127 - 26.3 per cent chrome - and
   inside that header the record label had a clientWidth of 125 for a
   scrollWidth of 397, so "Estate of Krishnan a/l Raman, deceased - letters of
   administration" rendered as "Estate of Krish...". The three header tools are
   Print, Download PDF and EMAIL RECORD. Email Record sends a privileged client
   file outside the firm, and on the one form factor where the reader has no
   other context at all - the list behind is covered, the topbar is covered -
   it sat beside a label that could not say which file was loaded.

   TWO THINGS ARE FIXED HERE.

   1. The label takes its own row below the tools and wraps to two lines. At
      375px that is 257px of width instead of 125px, and the label's
      clientWidth and scrollWidth are now equal, i.e. nothing is cut. It costs
      31px of header height - measured, 69px to 100px, taking the chrome from
      26.3 to 30.5 per cent of the dialog - and that is the right trade and is
      stated plainly rather than glossed: the question "which file is this
      control about to email out of the firm" has to be answerable, and 31px
      of a 744px dialog is what answering it costs. The rest of the chrome
      budget is the footer, which stacks Close above Open Page on a phone by
      house rule, and the overflow menu that would fold three tools into one
      lives in assets/app.js.
      Putting the tools on the row's left also puts them as far from the X as
      the bar allows, which is the separation the note beside .rec-tools in
      assets/app.js says it wants and only half achieved.

   2. `.rec-tool` measured 36 wide by 44 tall on a coarse pointer. That is the
      exact defect that was found and fixed on `.dialog-x`, and the comment
      beside that fix says "its sibling .rec-tool was given exactly this
      treatment explicitly" - it was not. `.rec-tool` is declared 36x36 and
      only its HEIGHT was ever stretched, so all three of these controls have
      been under the CR-3 floor on the width axis the whole time. This is the
      standing rule on this codebase read the hard way: the comment explains a
      fix that was not made.

   STILL OPEN, and both need assets/app.js, which this agent does not own:
     - the dialog's aria-label is the constant string 'Record', while
       describe() at app.js:965 already computes the record's real title one
       line away;
     - Email Record fires with no confirmation naming the matter.
   --------------------------------------------------------------------------- */
@media (pointer: coarse), (max-width: 900px) {
  .rec-tool {
    width: 44px;
    height: 44px;
  }
}

@media (max-width: 700px) {
  .ls-dialog.record .rec-head-bar {
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: flex-start;
    row-gap: 4px;
    padding-top: 8px;
    padding-bottom: 8px;
  }

  .ls-dialog.record .rec-head-bar > .rec-tools {
    order: 1;
  }

  /* -webkit-line-clamp is the only cross-engine way to cap a wrapping box at
     two lines. It needs the -webkit-box display, which is why the display is
     restated here rather than left as the block it would otherwise be. */
  .ls-dialog.record .rec-head-bar > p {
    order: 2;
    flex: 1 0 100%;
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: 1.35;
  }
}

/* ===========================================================================
   THE LEFT RAIL, 21 AUGUST 2026 - measured, then fixed.
   ===========================================================================

   WHAT WAS MEASURED. A partner's rail held 37 rows and rendered 1791px tall
   inside a 900px column: 17 rows on screen, 20 below the fold, in a scroll
   region of its own with nothing on screen saying there was more. Standing on
   Firm Management, the active row was laid out at y=1712 and the rail did not
   scroll it into view, so on a third of the application the one thing the rail
   exists to tell you - where you are - was off screen.

   THREE CHANGES, in the order they matter.

   1. THE IDENTITY BLOCK NO LONGER SCROLLS AWAY. `.side` was one scroll box
      holding the brand, the firm's name, the person, their seat, and the list.
      Scroll it and the firm's name left the screen; the topbar deliberately
      does not repeat it (see the note beside `.topbar` - a truncated second
      copy reading "Tan, Ra..." was removed for good reason). So on a
      multi-tenant product where an owner may hold several practices, the
      answer to "whose matters are these" could be scrolled out of existence.

      `.side` is now a flex column that does not scroll; only `.sidenav`, the
      list, does. The brand and `.sidewho` sit above it and stay. This is still
      the ordinary grid column - nothing is fixed, nothing is sticky, and
      nothing can cover a ledger figure.

   2. COMPACT ROWS WHERE THE POINTER IS FINE. CR-3 in the design protocol
      resolves density by pointer capability, not by guessing which screens go
      mobile: 44px is a floor on a COARSE pointer, and a pointer-primary
      operator surface takes the compact tier. The 44px floor was being applied
      to a mouse as well, which cost 240px of rail height on a desktop for no
      one's benefit. Rows are 34px on a fine pointer and stay 44px the moment
      the pointer is coarse, where the rule actually binds.

   3. Together with folding 37 destinations into 24 rows (see nav_groups()),
      the rail is 1339px instead of 1791px and about five rows sit below the
      fold at 900px instead of twenty - and assets/panel-nav.js now scrolls the
      current row into view on load, so those five are never the one you are
      standing on.
   ------------------------------------------------------------------------ */
@media (min-width: 901px) {
  .side {
    overflow: hidden;
    min-height: 0;
  }

  /* min-height:0 is what lets a flex child actually shrink below its content
     height and scroll. Without it the list keeps its full 1339px and pushes
     the identity block back off the top, which is the bug this is fixing. */
  .side > .sidenav {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* The last row must not sit flush against the bottom edge, and
       scrollIntoView() must not park it there either - hence the scroll
       padding as well as the box padding. The house rule is spacing, always;
       a row touching an edge reads as a row that has been cut off. */
    padding-bottom: 14px;
    scroll-padding-block: 10px;
    scrollbar-width: thin;
    scrollbar-color: rgb(255 255 255 / .28) transparent;
  }

  .side > .sidenav::-webkit-scrollbar { width: 10px; }
  .side > .sidenav::-webkit-scrollbar-thumb {
    background: rgb(255 255 255 / .24) padding-box content-box;
    border: 3px solid transparent;
    border-radius: 6px;
  }
  .side > .sidenav::-webkit-scrollbar-thumb:hover {
    background: rgb(255 255 255 / .4) padding-box content-box;
  }
}

/* CR-3: the compact tier is for a pointer-primary surface, and only there. */
@media (min-width: 901px) and (pointer: fine) {
  .sidenav a {
    min-height: 34px;
    padding-top: 6px;
    padding-bottom: 6px;
    line-height: 1.25;
  }

  .sidenav ul { margin-bottom: 12px; }

  .navgroup {
    margin-bottom: 3px;
    font-size: 10px;
  }
}

/* ===========================================================================
   TWO STRIPS, TWO MEANINGS. See page_tabs() in lib/layout.php.
   ===========================================================================

   `.tabs`   a LOCATION - a different screen or a different data set.
   `.subnav` a FILTER - a slice of the list already on the page.

   Both were already drawn. What is added here is only what was needed once
   they started appearing on the same page: the filter must not stretch to the
   full width of the content, or a four-item segmented control reads as a
   second full-width tab bar sitting under the first, which is the exact
   confusion this separation exists to remove. `nav.subnav` rather than
   `.subnav` because the built stylesheet's own `@layer components` copy of the
   old folder-tab rule still carries `display:flex`.
   ------------------------------------------------------------------------ */
nav.subnav {
  display: inline-flex;
  width: auto;
  max-width: 100%;
}

/* The section strip sits ABOVE the heading and introduces it, so it carries a
   little space beneath rather than the 20px it used when it was a strip in the
   middle of a page. */
.tabs {
  margin-bottom: 14px;
}

.page-head + nav.subnav {
  margin-top: -4px;
}

/* Below 700px a segmented control with four or five slices cannot shrink-wrap
   without the labels colliding, so it takes the full width and shares it. */
@media (max-width: 700px) {
  nav.subnav { display: flex; width: 100%; }
}

/* ===========================================================================
   THE MOBILE DRAWER, TWO COLUMNS.
   ===========================================================================

   The drawer is normal-flow by design - it pushes the page down rather than
   covering it, because nothing in this product may cover a ledger figure. That
   is right, and it means its HEIGHT is the whole cost of opening it. Measured
   at 375px on 21 August 2026: 1809px of links before this pass, pushing the
   page content to y=1987, so a lawyer who opened the menu to reach Firm
   Management scrolled two full screens to it and two back to close it.

   Folding 37 destinations into 24 rows took that to 1312px. Laying those rows
   out in two columns takes it to roughly half again, with no new gesture to
   learn, no JavaScript, and the 44px coarse-pointer target untouched: the row
   keeps its full height and only gets narrower.

   The group headings span both columns, so Money still starts a block of its
   own and the reading order down each group is preserved.
   ------------------------------------------------------------------------ */
@media (max-width: 900px) {
  .sidenav ul {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2px 6px;
    /* A label that wraps makes its own row taller rather than being clipped -
       "Assistant Supervision" has to be readable in a 150px column. */
    grid-auto-rows: minmax(44px, auto);
    align-items: stretch;
  }

  .sidenav li { margin-bottom: 0; }

  .sidenav a {
    height: 100%;
    padding: 8px 10px;
    line-height: 1.2;
  }

  .navgroup { grid-column: 1 / -1; }
}

/* A phone narrow enough that two columns would truncate goes back to one. */
@media (max-width: 359px) {
  .sidenav ul { grid-template-columns: minmax(0, 1fr); }
}

/* ===========================================================================
   THE SEARCH FIELD GETS ITS OWN ROW ON A PHONE.
   ===========================================================================

   Measured at 375px on 21 August 2026: `#gq` laid out at 152px wide with 34px
   of left padding for the magnifier and 12px on the right, leaving 106px for
   the placeholder. "Search clients, matters, people" needs 224px, so it
   rendered as "Search clien" - and the field that reads "Search clien" is the
   one control on the screen that has to say what it searches. The topbar was
   `flex-wrap:nowrap` below 900px, so the search, the assistant button and the
   account menu fought over one 375px line and the search lost.

   Nothing is removed to fix it. The account row keeps its line, the search
   takes the next one in full, and the placeholder is read in full: 343px of
   line against 224px of text. This is normal flow, above the content, and it
   scrolls away with it like everything else in this shell.
   ------------------------------------------------------------------------ */
@media (max-width: 700px) {
  .topbar {
    flex-wrap: wrap;
    row-gap: 10px;
  }

  /* Left, not right. With the name and the role pill hidden at this width the
     group is two controls, and pinned right they sat at the end of an empty
     line above a full-width search box, which reads as something failed to
     load. Along the same edge as the search below them they read as one
     toolbar, which is what they are. */
  .topbar > .who {
    order: 1;
    margin-left: 0;
  }

  .topbar > .gsearch {
    order: 2;
    flex: 1 0 100%;
    min-width: 0;
  }

  .gsearch input { max-width: none; }
}

/* The palette row for the screen already on the display. It is not removed -
   a list that changes shape from page to page is a list nobody can learn - but
   it must not read as a door to somewhere else. See command_palette(). */
.cmdk-list a.is-here {
  opacity: .72;
}

.cmdk-list a.is-here .cmdk-grp {
  color: var(--brand);
  font-weight: 600;
}

/* ===========================================================================
   THE CONTENTS LIST. See page_contents() in lib/layout.php.
   ===========================================================================
   A quiet block, not a card: it is apparatus for reading the page, not part of
   the record on it. Laid out in columns so twenty-three headings are a block
   the eye takes in at once rather than a second wall above the first.
   ------------------------------------------------------------------------ */
.pagecontents {
  margin: 0 0 18px;
  padding: 14px 16px;
  background: var(--surface-sunk, #f3f6f9);
  border: 1px solid var(--line);
  border-radius: var(--r-sm, 8px);
}

.pagecontents-t {
  margin: 0 0 8px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .055em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.pagecontents ul {
  margin: 0;
  padding: 0;
  list-style: none;
  columns: 3 200px;
  column-gap: 24px;
}

.pagecontents li {
  break-inside: avoid;
  margin: 0 0 4px;
}

.pagecontents a {
  color: var(--brand-2, #1b5488);
  font-size: 13.5px;
  line-height: 1.35;
  text-decoration: none;
  display: inline-block;
  padding: 3px 0;
}

.pagecontents a:hover { text-decoration: underline; }

/* CR-3: the 44px floor binds on a coarse pointer, and a column of jump links
   is exactly the sort of dense list that fails it. */
@media (pointer: coarse) {
  .pagecontents ul { columns: 1; }
  .pagecontents a { min-height: 44px; display: flex; align-items: center; }
}

/* ===========================================================================
   THE SMALL SET OF UTILITIES THE PAGES WERE HAND-ROLLING.
   ===========================================================================

   CR-2 in the design protocol: no one-off styling; a repeated decision belongs
   in the system, expressed as a token, a variant or a class. Counted across the
   application's own pages on 21 August 2026, EXCLUDING the email and PDF
   builders which have to inline everything:

       529 inline style="" declarations, of which
       114 were the same one - font-size:13px - with no class behind it,
        62 were a top margin of 12, 14, 16 or 18px,
        26 a bottom margin of 14 or 16px,
        24 display:inline and 22 white-space:nowrap.

   A value written 114 times by hand is not a decision, it is a default nobody
   named. Naming it is what makes it changeable: a firm re-theming the product,
   or a later pass raising the small size for legibility, has one place to edit
   instead of 114, and none of the 114 can drift from the others in between.

   These are deliberately spare - a size, four spacing steps that already exist
   as --s3..--s5, and two layout switches. This is not a utility framework and
   must not grow into one; anything that repeats beyond these earns a real
   component, not another class.
   ------------------------------------------------------------------------ */
/* LITERAL VALUES, NOT var(--sN), AND THAT IS DELIBERATE.
   These first read `var(--surface-active, 12px)` and `var(--s4, 16px)`, which looked like
   the right thing - use the spacing scale rather than a magic number. It was
   wrong, and silently: --s2 and --s3 are each declared THREE times in this
   stylesheet and the later two declare them as COLOURS (--s3 is #f1f5f9 in
   light and #1a2735 in dark; --s2 is #f7f9fb / #111b26). The later :root wins,
   so `margin-top: var(--surface-active, 12px)` computed `margin-top: #f1f5f9` - invalid at
   computed-value time, so the declaration becomes unset and the margin is 0.
   The 12px fallback never fires, because a fallback only applies when the
   custom property is UNDEFINED, and --s3 is very much defined.
   Measured in a browser: .mt-3 gave marginTop 0px on calendar.php,
   calendar_settings.php and matter_view.php, pushing the Apply Sources and Save
   States buttons flush against the control above them.
   The spacing scale in this file is not trustworthy as a set of names, so these
   nine utilities carry their own values.
   NAMED FOR THE VALUE, NOT FOR A GUESS. These were first called .mt-45/.mb-45,
   as if 14px sat between step 4 and step 5. It does not: step 3 is 12px and
   step 4 is 16px, so 14 sits between THREE and four. A utility whose name
   contradicts its value is a trap for whoever reaches for the next one. See the note on app.css being hand
   edited after the Tailwind build - the 119 redefinitions are a known
   outstanding issue and this is one of the ways they bite. */
.t-sm  { font-size: 13px; }

.mt-3  { margin-top: 12px; }
.mt-4  { margin-top: 16px; }
.mt-35 { margin-top: 14px; }
.mt-5  { margin-top: 18px; }

.mb-4  { margin-bottom: 16px; }
.mb-35 { margin-bottom: 14px; }

.d-inline { display: inline; }
.nowrap   { white-space: nowrap; }

/* ===========================================================================
   THE LEFT RAIL MUST NEVER SCROLL SIDEWAYS. 24 August 2026.
   ===========================================================================

   REPORTED WITH A SCREENSHOT: a horizontal scrollbar across the bottom of the
   rail, under the nav list, on a desktop window.

   WHY IT IS THERE AT ALL. The rail's list was given `overflow-y: auto` and
   nothing else. CSS overflow does not let one axis stay `visible` while the
   other scrolls: per the spec, a computed `visible` on one axis becomes `auto`
   when the other axis is not `visible`. So `.side > .sidenav` has been
   `overflow-x: auto` the whole time by accident, never by decision, and any
   sub-pixel of horizontal overflow - a fractional column width under browser
   zoom or a display scale, a wider classic scrollbar on the vertical axis
   eating into the row width, a label with no space in it in a firm's own
   wording - draws a real scrollbar. The same accident applies to `.side`
   itself below 901px, where it is the `overflow-y: auto` bar.

   A horizontal scrollbar on a fixed-width rail is never useful: there is no
   destination to its right, and it steals ~11px of the last row's height on a
   list that is already scrolling vertically.

   TWO PARTS, because hiding a scrollbar is not the same as fixing an overflow.

   1. The axis is closed explicitly on both boxes. `hidden`, not `clip`, so a
      programmatic scrollIntoView() on a nav row still behaves.

   2. The content is made unable to overflow in the first place: the row is a
      flex line whose label may shrink (`min-width: 0` was already on
      `.navlabel`, but the anchor itself had no floor released), and a long
      unbroken word now breaks instead of poking out past the edge. Without
      this the label would simply be CUT by part 1 - absence dressed up as a
      fix - and "Firm Management" reading "Firm Manageme" is the defect this
      product's own rules on truncation forbid.

   Verified after the change at 1440, 1280, 1100, 901, 900, 768 and 375px:
   scrollWidth equals clientWidth on `.side` and on `.sidenav` at every one,
   the vertical scroll of the list still works, and no label is clipped.
   ------------------------------------------------------------------------ */
.side,
.side > .sidenav {
  overflow-x: hidden;
}

.sidenav a {
  min-width: 0;
}

.navlabel {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The identity block above the list is plain markup with no rules of its own,
   so a firm whose name carries a long unbroken token (a bare domain, a long
   Bahasa compound) would push the same overflow into `.side`. Same treatment,
   for the same reason: wrap it, do not cut it. */
.sidewho,
.sidewho-firm,
.sidewho .firmname {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A stat tile that has to qualify its own figure. See the note beside
   Fees::unpricedNote(): "0.00 Value at your rate" is a lie when the truth is
   that nothing on the screen could be priced at all, and the correction has to
   sit with the figure rather than in a banner somewhere else on the page.
   Sized below the label so the tile still reads as a number first. */
.stat-note {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
  text-wrap: pretty;
}

/* ===========================================================================
   THE ADMIN CONSOLE'S FLAT LIST: TEN COLUMNS THAT READ AS THREE BLOCKS.
   ===========================================================================
   "Owners and their law firms" is one row per law firm, with every fact of both
   the firm and the account that holds it in a column of its own, so any of them
   can be compared down the page.

   Two earlier shapes are worth recording, because both are easy to reach for
   again. A SEPARATE TABLE PER OWNER repeated the column names once per account
   and gave a screen reader N tables with one name. ONE TABLE WITH A BAND ROW
   per account fixed that, but put the account's facts in a run-on sentence
   while the firm's facts stayed in columns - two kinds of reading on one
   surface, and nothing in the band could be compared down a column, which is
   the only reason to use a table at all.

   Ten columns is a lot to hand somebody at once, and the answer is not fewer
   columns. It is saying where one subject ends and the next begins.

   THE COLUMN RULES ARE GONE, AND `.col-split` WITH THEM. 27 Aug 2026.

   Two vertical hairlines marked "where the firm's facts start" and "where the
   controls start". They cost no width and they were still wrong: no other table
   in LawServa rules a column, so this one alone read as a spreadsheet rather
   than as a list of records, and the rule in front of the action column fenced
   the two controls off from the row they act on. Row separators are the grain a
   data table is scanned along; column separators fight them.

   What the rules were compensating for was the real defect - nine columns on
   declared percentage widths, so short columns held dead air and long ones
   wrapped, and the eye needed a fence to find its way across the gaps. Widths
   are content-driven now (below) and there is nothing left to fence.

   The class had exactly one caller, this table. Deleted rather than left
   behind: a rule nothing uses is a rule the next reader has to prove is dead.
   ------------------------------------------------------------------------ */

/* An email is the one column here whose content has no natural ceiling. It
   wraps inside its own width rather than being cut off with an ellipsis: this
   is a billing address somebody is about to write down, and a value you have to
   hover to finish reading is a value the table did not actually give you. */
.col-email { max-width: 210px; min-width: 156px; overflow-wrap: anywhere; }

/* A DATE is read as one token and never breaks across lines. A date RANGE is
   two of them and a word, and it does not fit.

   `white-space: nowrap` on the whole cell was measured overflowing its own box
   on 26 August 2026: the column is 14% of the table, which is 159px at a
   1440px window, and "09/08/2026 to 09/08/2027" is about 163px. The text did
   not wrap - it spilled across the Duration column beside it, so the operator
   read "09/08/2027365".

   `nowrap` on the DATES and a normal break at the spaces is what was actually
   wanted. There is no space inside 09/08/2026, so each date is already
   unbreakable on its own; all the cell has to do is stop forbidding the break
   between them. Two short lines in a narrow column beat one line laid over the
   number next to it. */
.col-term { white-space: normal; overflow-wrap: normal; }

/* NINE COLUMNS, SIZED BY WHAT IS IN THEM. Rewritten 27 Aug 2026.

   WHAT WAS HERE AND WHY IT WENT. `table-layout: fixed` plus nine percentages
   totalling 100. Every one of them was measured honestly, and the shape was
   still wrong, because a percentage is a share of the table and not a
   measurement of a column's content: the two countdown columns were 8% each -
   115px at a 1440px window - to hold three digits, while a five-word Kuala
   Lumpur firm name wrapped onto three lines in the 13% beside them. Roughly
   180px of dead air on every row, taken from the two columns that had the most
   to say. That whitespace is the thing this table was reported for.

   Fixed layout was reached for because auto layout, left alone, hands width out
   in proportion to content and gives a one-digit "No." more room than it can
   use. The answer is not to declare all nine - it is to pin the four that have
   a natural ceiling and let the five that do not share what is left.

   `width: 1px` is shrink-to-content: a column can never be narrower than its
   own min-content, so the declaration floors at "as narrow as this will go".
   On the TD only, never the TH - the headings here wrap (below), so a nowrap on
   both would make "Duration (Days)" set a 100px floor for a column holding
   "365". A column is as wide as the widest thing in it, and here that must be
   the value, not the heading.

   The earlier note in this file said `width: 1%` on the short columns "makes it
   worse", and it did - applied to short columns under FIXED layout, where the
   long columns then pay for the nowrap floor. Under auto layout the mechanism
   is the opposite one. Recorded because the two look identical in a diff.
   ------------------------------------------------------------------------ */
/* THE FLOOR HAS TO OUT-SPECIFY THE SIX-COLUMN FLOOR, and until 26 August 2026
   it did not - so neither this table's 1100px nor the 1040px before it was ever
   in force. Measured at a 1280px window: computed min-width 720px.

   `.table-scroll table:has(thead th:nth-child(6)){min-width:720px}` is (0,2,3),
   because :has() takes the specificity of its argument, and a bare `.tbl-owners`
   is (0,1,0). Later in the file does not help: specificity is decided first.
   This is the same trap, and the same fix, as the stacked-table rule near
   `STACKED TABLE: beat the six-column floor` - written out again here rather
   than shared, because the two need DIFFERENT winners.

   `.table-scroll table.tbl-stack.tbl-owners` is (0,3,2) and wins. Both classes
   are in the server-rendered markup, so this holds before any script runs.

   INSIDE A MIN-WIDTH QUERY, WHICH IS THE LOAD-BEARING HALF. The stacking rule
   that flattens this table on a phone is ALSO (0,3,2), and this block sits
   later in the file, so at equal specificity this would win and a 375px handset
   would get a wide table inside a 375px card. A media query adds no specificity
   - it decides whether the rule exists at all - so the two are kept apart by
   width rather than by a specificity race neither can win cleanly.

   880, DOWN FROM 976. The floor exists so the action column is never behind a
   horizontal scroll on the narrowest screen this page is worked on: a LawServa
   operator at 1280 gets a 979px content column once the 236px rail is off. The
   action cell went from five icon buttons to two worded ones - measured 168px
   against 146px - and the four pinned columns give back more than that, so the
   floor comes down rather than up. Verified at 1024 / 1280 / 1440. */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-owners { min-width: 880px; }
}
.tbl-owners { min-width: 880px; }

/* The headings wrap, the values do not. Nine headings on one line is 100px of
   unbreakable label over columns holding two characters; "SUBSCRIPTION PERIOD"
   on two lines costs one row of header height, once. */
.tbl-owners thead th { white-space: normal; vertical-align: bottom; }

/* 10px OF SIDE PADDING, NOT 12, AND ONLY HERE.
   Nine columns pay the cell padding nine times. This is a pointer-primary
   operator table - CR-3's compact tier - so density is the right thing to
   spend, and the vertical padding, which is what actually separates the ROWS a
   person is scanning, is untouched. It was 8px while the widths were declared
   and every pixel was owed to a percentage; with four columns pinned to their
   content there is room to give the gutters back some air. */
.tbl-owners td, .tbl-owners th { padding-left: 10px; padding-right: 10px; }

/* THE PERIOD OPENER IS A QUIET LINK.
   `.linkbtn` is brand-2 at 600 weight, which is right for the one primary
   record link in a row and wrong for a column that carries a date on EVERY
   row: fourteen bold blue date ranges down a page is a stripe of emphasis over
   a value nobody is being asked to click first. The affordance stays - a
   dotted underline is the "there is more behind this" mark - and the emphasis
   is spent on hover and focus, where it is about one row rather than all of
   them. Colour is not the only channel either way. */
.tbl-owners .col-term .linkbtn {
  color: inherit;
  font-weight: inherit;
  text-decoration: underline dotted var(--line);
  text-underline-offset: 3px;
}
.tbl-owners .col-term .linkbtn:hover,
.tbl-owners .col-term .linkbtn:focus-visible {
  color: var(--brand-2);
  text-decoration: underline solid currentColor;
}

/* THE FOUR PINNED COLUMNS: a row number, two counts of days, and the controls.
   Each holds something with a known ceiling, so each takes exactly what it
   holds and the five text columns share everything else.

   INSIDE THE SAME WIDTH QUERY AS THE FLOOR, and that is load-bearing rather
   than tidy. A column width means nothing once the table is a list of blocks,
   and `.tbl-owners td:nth-child(9) .acts` is (0,3,1) - it beats anything the
   stacked block can reasonably write to undo it. A media query adds no
   specificity; it decides whether the rule exists at all, which is the only
   reliable way to keep a grid rule off a surface that has no grid. */
/* BY CLASS AND NEVER BY POSITION, and that is not a preference since 27 Aug
   2026: the person's four cells carry a rowspan, so a continuation row holds
   five cells whose first is the FIFTH column. `td:nth-child(1)` would have
   pinned the Law Firm Name column to its own min-content on every row after
   the first of a group. nth-child counts siblings; it cannot see a grid. */
@media (min-width: 761px) {
  .tbl-owners .c-no,
  .tbl-owners .c-days,
  .tbl-owners .row-actions { width: 1px; white-space: nowrap; }

  /* The worded controls sit on one line or the row grows 30px. `.acts` wraps by
     default, which is right in a narrow cell and wrong in one that is sized to
     its own content. */
  .tbl-owners .row-actions .acts { flex-wrap: nowrap; }
}

/* ===========================================================================
   THE OWNER CONSOLE'S LAW FIRMS LIST. 27 Aug 2026.
   ===========================================================================
   Nine columns, the same shape and the same technique as the Admin Console's
   Owners table above: content-driven widths, with only the columns that have a
   natural ceiling pinned to their own content. See the long note there for why
   declared percentages were the wrong tool - the short version is that a
   percentage is a share of the table and not a measurement of a column, so
   three-digit counts sat in dead air while firm names wrapped beside them.

   NO `table-layout: fixed`, NO PERCENTAGES, and by class rather than
   nth-child so the rules survive anything that changes the cell count.

   The floor is 860, below the Owners table's 880 because this list has no
   action column to protect - one worded Details button, not a control set -
   and a customer reads it on whatever they own rather than on a support desk's
   1280 laptop. Verified at 1024 / 1280 / 1440 and at 390.
   ------------------------------------------------------------------------ */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-firms { min-width: 860px; }

  /* The counts and the countdown: each holds something with a known ceiling.
     `min-width` as well as `width: 1px`, because on this list the ceiling is
     the HEADING, not the value: the cells hold "365" and a countdown pill, so
     shrink-to-content sized the column from "EXPIRE" alone and the heading
     broke over three lines. 84px is "EXPIRE IN" plus the gutters - two lines,
     which is what the Admin Console's identical heading does. */
  .tbl-firms .c-days { width: 1px; min-width: 84px; white-space: nowrap; }
  /* A count with a short heading needs no floor at all - see the note on the
     cell. Shrink to content and stop. */
  .tbl-firms .c-count { width: 1px; white-space: nowrap; }
  .tbl-firms .row-actions { width: 1px; white-space: nowrap; }

  /* The subscription's spanned cell. It carries the account's name, its state,
     its dates, its grace and its billing address stacked in one column, so it
     is sized from the widest of those rather than shrink-to-fit like the
     numeric columns beside it. */
  /* `.c-sub` WAS HERE AND IS DELETED WITH ITS COLUMN, 27 Aug 2026. The
     subscription is a property of the law firm, so it is on the firm's row -
     see the note in law_firms.php. A rule nothing uses is a rule the next
     reader has to prove is dead. */
  .tbl-firms .c-firm { min-width: 150px; }
}

/* THE GROUP SEPARATORS WENT WITH THE GROUPS. This is one row per law firm
   again, in one <tbody>, so the ordinary row rule is the right one and a
   heavier line at the end of the body would be a boundary around nothing. */

/* The switcher and Edit sit in the card head beside the title, so neither
   stretches the way a filter row does on its own. */
.card-head > .acts .rowform, .card-head > .rowform { margin: 0; }
.card-head > .acts { gap: 8px; }
.card-head select { min-width: 200px; }
.tbl-firms { min-width: 860px; }

/* Nine headings on one line is 100px of unbreakable label over columns holding
   two characters. Wrapping them costs one row of header height, once. */
.tbl-firms thead th { white-space: normal; vertical-align: bottom; }

/* Nine columns pay the cell padding nine times; this is the compact tier. */
.tbl-firms td, .tbl-firms th { padding-left: 10px; padding-right: 10px; }

/* The firm name is the widest thing here and the column it lives in is the one
   the reader scans. Measured from a five-word Kuala Lumpur practice name. */
.tbl-firms td:first-child { min-width: 168px; }

/* The period opener is a link that must not shout - the same treatment, and the
   same reasoning, as the Admin Console's. A dotted underline is the "there is
   more behind this" mark; the emphasis is spent on hover and focus, where it is
   about one row rather than all of them. */
.tbl-firms .col-term .linkbtn {
  color: inherit;
  font-weight: inherit;
  text-decoration: underline dotted var(--line);
  text-underline-offset: 3px;
}
.tbl-firms .col-term .linkbtn:hover,
.tbl-firms .col-term .linkbtn:focus-visible {
  color: var(--brand-2);
  text-decoration: underline solid currentColor;
}

@media (max-width: 760px) {
  .tbl-firms.is-stacked { min-width: 0; }
  .tbl-firms.is-stacked td:first-child { min-width: 0; }
}

/* ---------------------------------------------------------------------------
   ONE OWNER, ONE GROUP. 27 Aug 2026.

   Each owner is a <tbody> holding one row per law firm, with the person's four
   columns spanning them. Two separators, not one: a soft hairline between the
   practices of one owner, a full line where the next owner starts. Without the
   second, eleven practices under four owners is eleven identical rows again and
   the grouping is invisible; with a heavier rule INSIDE the group as well, the
   page reads as a grid of boxes rather than as a list of people.
   ------------------------------------------------------------------------ */
.tbl-owners tbody > tr:last-child > td { border-bottom: 1px solid var(--line); }
.tbl-owners tbody:last-child > tr:last-child > td { border-bottom: 0; }

/* AND AIR, BECAUSE TWO WEIGHTS OF HAIRLINE ARE NOT ENOUGH ON THEIR OWN.
   --line over --line-soft is #dde3ea against #eef1f5: a real difference at 100%
   and not one an operator scanning a page of fourteen practices will ever
   consciously read. The boundary is a line AND a gap; the gap is what the eye
   actually groups on. 6px at each end of a group, taken as padding rather than
   margin because a table row cannot carry a margin. */
.tbl-owners tbody > tr:first-child > td { padding-top: 16px; }
.tbl-owners tbody > tr:last-child > td { padding-bottom: 16px; }
.tbl-owners tbody:first-child > tr:first-child > td { padding-top: 12px; }

/* The name is the widest thing an owner has and the column kept coming out
   narrowest, because a cell that spans rows contributes nothing to how auto
   layout sizes its column in any current engine: "Lim Chee Keong" wrapped onto
   two lines at 120px while the address beside it sat unbroken at 168. Measured
   from a Malaysian full legal name, which is what this column holds.

   150 AND 156, NOT 156 AND 168, and the twelve pixels are the whole point.
   Measured at 1280 - the narrowest width this page is actually worked on, and
   the one where a 236px rail leaves a 979px content column - the table came out
   989px wide, so the last ten pixels of the Action column sat behind a
   horizontal scroll inside the card. Ten pixels of a name wrapping a word
   sooner costs a reader nothing; ten pixels of a control disappearing costs
   them the reason the screen exists. Re-measured after: 977. */
.tbl-owners .c-name { min-width: 150px; }

/* The person's cells sit at the top of their group and stay there. A name
   centred against three practices floats away from the first line it belongs
   to; a name at the top is beside the row a reader starts on. */
.tbl-owners .c-name, .tbl-owners .c-no,
.tbl-owners .c-phone, .tbl-owners .col-email { vertical-align: top; }

/* HOVER FOLLOWS THE GROUP, because the row under the pointer is only part of
   what a reader is looking at: hovering the third practice of an owner with the
   name cell left unlit reads as though the name belongs to somebody else. The
   `is-inside` tint is restated after it - it says which practice this operator
   is currently inside, which outranks a pointer being nearby. */
@media (hover: hover) and (pointer: fine) {
  .tbl-owners tbody:hover > tr > td { background: var(--s-hover); }
}
.tbl-owners tbody > tr.is-inside > td,
.tbl-owners tbody:hover > tr.is-inside > td { background: var(--brand-tint); }

/* BREAK-ANYWHERE IS A FIXED-LAYOUT RULE AND IT IS POISON UNDER AUTO LAYOUT.
   `overflow-wrap: anywhere` sets a cell's min-content width to ONE CHARACTER,
   and auto layout sizes a column from its min-content: told that every column
   can be 1ch wide, the browser squeezed the addresses to about 90px and printed
   "peter@vyrox.co / m" on two lines while the row had 300px going spare.
   Measured at 1440 on 27 Aug 2026, immediately after the widths came off.

   It is kept for the ONE column that needs it - an email address has no space
   to break at, so without it a long address would set a floor the whole table
   pays for - and that column is capped, so the break has somewhere to happen.
   Everything else here breaks at spaces like ordinary prose. */
.tbl-owners .col-email { overflow-wrap: anywhere; }

/* An icon-only control still needs a target a finger can hit. .btn.icon.small
   is 28px, which is right beside a pointer and below the 44px floor on a coarse
   one, so the row gives way rather than the target.

   IT SAID 44 AND IT DID 40, until 28 Aug 2026 - the comment named the floor
   this protocol actually holds and the rule below it stopped four pixels short,
   which is a target that fails the check its own note claims to pass. Now 44.

   AND IT COVERS THE OWNER CONSOLE'S LAW FIRMS TABLE TOO. That table's action
   column became icon-only the same day (Alex: "learn from admin console, use
   flaticons also for actions at action column for the law firm table"), and a
   rule scoped to one console would have given the same control two different
   sizes on the same phone. */
@media (pointer: coarse) {
  .tbl-owners .acts .btn.icon.small,
  .tbl-firms  .acts .btn.icon.small { width: 44px; height: 44px; }
}

@media (max-width: 760px) {
  .tbl-owners.is-stacked { min-width: 0; }

  /* The pinned-column rules above are switched off by width, not fought on
     specificity - see the note beside them. Nothing here has to undo them. */
  .tbl-owners.is-stacked td { white-space: normal; }
}

/* The header over the action column. `th` is left-aligned like every other
   column name, and .row-actions is right-aligned, so "ACTIONS" sat at the far
   left of a column whose controls are at the far right - naming a column it
   did not appear to be over. */
.col-act { text-align: right; }

/* ---------------------------------------------------------------------------
   THE ACTION COLUMN.

   Icons are INLINE SVG drawn in ui_icon(), never an icon font and never a CDN:
   a signed-in LawServa screen must not tell a third party which firm opened
   which console and when. See the note on that function.

   Icon AND label, not icon alone. Every control in this column acts across
   tenants - it opens somebody else's client files, or hands a practice to an
   account that will be billed for it - and a glyph whose meaning has to be
   remembered is not an honest way to offer that. The icon is the fast channel
   for a reader who already knows the row; the word is the one that is right
   the first time.
   ------------------------------------------------------------------------ */
.acts {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  flex-wrap: wrap;
}
/* Leave POSTS - it does not open a dialog - so it needs a form around it.
   `display: contents` would be the tidy answer and is not used: several
   screen readers still drop an element removed that way out of the tree, and
   this is a control that ends a support visit. An inline-flex form is one
   extra box and costs nothing. */
.acts form { display: inline-flex; margin: 0; }

/* `.btn.danger{margin-left:auto}` exists for a DIALOG FOOTER, where the
   destructive control is deliberately pushed away from Close so a mis-aimed
   click cannot reach it. Inside a row's action group that same rule shoves the
   delete icon to the far end of the flex line and forces the icons before it
   onto a second row - which is what a table of thirteen rows growing forty
   pixels of height looked like. The separation is right in a footer and wrong
   in a 120px cell, so it is undone here rather than weakened there. */
.acts .btn.danger { margin-left: 0; }

/* The row of the firm this operator is CURRENTLY INSIDE.
   The table could not say this before: the banner at the top of the page named
   the firm, and the row for that same firm still offered "Enter Firm" as
   though nothing were open. Position and a word, not a colour on its own. */
tbody tr.is-inside > td { background: var(--brand-tint); }
tbody tr.is-inside:hover > td { background: var(--brand-tint); }
.row-name { font-weight: 600; color: var(--ink); }

@media (max-width: 760px) {
  /* A TH inside <tbody> is a row header, not a column name - the summary
     tables use one to label a row. The stacking rule above names `td` only, so
     without this it stays a table-cell inside a display:block row and sits on
     its own line at the wrong width. */
  .tbl-stack.is-stacked tbody th {
    display: block;
    border-radius: 0;
    padding: 10px 12px;
    margin: 0 0 10px;
  }
  /* Actions read left-to-right in a stacked block, like every other field in
     it. `.row-actions` is right-aligned for the grid, which is the wrong edge
     once the grid is gone. */
  .tbl-stack.is-stacked td.row-actions { text-align: left; }
  .acts { justify-content: flex-start; }
}

/* A paragraph in a card head that explains the whole card rather than
   annotating its title. .card-head is a wrapping flex row, so without this the
   three lines of policy above the unowned-firms table were laid out as a
   squeezed column beside the heading. Full width, on its own line, under it. */
.card-head-full { flex: 1 0 100%; margin: 0; }

/* A card head inside a flush card owns the padding for everything in it,
   including the note above - the table below starts at the card's own edge. */
.card.flush > .card-head > .card-head-full { margin-top: 2px; }


/* ---------------------------------------------------------------------------
   STACKED TABLES: A CHIP IS A CHIP, NOT A BAR.

   `.tbl-stack.is-stacked td` is a two-column grid, and a grid item with no
   width of its own is stretched to its column. Every pill in every stacked
   table was therefore drawn as a full-width rounded bar down one side of the
   phone - "Suspended" rendered 370px wide - which reads as a progress track or
   a banner rather than as the small status chip it is on every other width.
   The same applied to a lone button in the action column.

   Named rather than blanket (`td > *` would also shrink-wrap the text blocks
   that legitimately fill their column), so this touches chips and controls
   only. It is a fix to the shared stacking pattern, not to one screen: every
   list in the product that carries a status renders correctly on a phone now.
   ------------------------------------------------------------------------ */
@media (max-width: 760px) {
  /* `.linkbtn` JOINED THIS LIST ON 27 AUG 2026, and it belongs to every table
     that opens a record from its own value, not to the Owners screen: a
     stretched grid item is a <button>, and a button lays its text out centred,
     so "23/08/2025 to 23/08/2026" sat in the middle of the value column while
     the plain text in the cells above and below it started at the left edge.
     Measured on a 390px viewport. `justify-content` as well as `justify-self`,
     because shrinking the box is not the same as un-centring what is in it. */
  .tbl-stack.is-stacked td > .pill,
  .tbl-stack.is-stacked td > .acts,
  .tbl-stack.is-stacked td > .btn,
  .tbl-stack.is-stacked td > .linkbtn,
  .tbl-stack.is-stacked td > form { justify-self: start; }
  .tbl-stack.is-stacked td > .linkbtn { justify-content: flex-start; }

  /* EVERY CHILD OF A STACKED CELL BELONGS UNDER THE VALUE, NOT UNDER THE LABEL.
     `td` is a two-column grid whose FIRST item is the `::before` carrying the
     field name. A cell with one value therefore lays out label | value and
     reads correctly. A cell with a value AND a qualifier - "28/08/2026" with an
     "In 2d" chip, "Expired" with "60 days ago", a term with "This firm's own
     term" - has three items, so the third wrapped onto a new row and landed
     back in the LABEL column, at the far left, reading as a field whose name
     had gone missing.
     Every one of those is a note ABOUT the value, so every one belongs in the
     value column: pinning the column is what says so. `*` rather than a list of
     the elements that happen to do it today - this was written for one table's
     `div`, and the Owners table's chips, the Matters due-date chip and the Law
     Firms term note each reproduced it in turn.
     grid-column only. Sizing is left to the rule above, which shrink-wraps
     chips and controls and lets genuine text blocks fill the column. */
  .tbl-stack.is-stacked td > * { grid-column: 2; }

}

/* ---------------------------------------------------------------------------
   LAW FIRMS WITH NO OWNER (the second card on admin.php).

   Five columns, not the Owners table's ten: these firms are on no account, so
   the five columns that describe one would all read "-". Wide enough not to
   need declared widths - `table-layout: auto` allocates sensibly at five - and
   the two rules it does need are the ones the table above needed for the same
   reason, a long practice name and a control that must not be squeezed.

   .tbl-firms WAS HERE, for a second operator screen called Law Firms. That
   screen was removed the same day it shipped (see platform_nav_groups) and its
   rules went with it rather than being left to match nothing.
   ------------------------------------------------------------------------ */
.tbl-orphans td { overflow-wrap: anywhere; }
/* NO width ON THE ACTION COLUMN. `width: 1%` gives a column its MIN-content
   width, and the min-content of a wrapping flex row is its widest single item -
   so the one row that carries two controls broke Attach and Enter Firm onto
   separate lines on a table with 300px to spare. At five columns the automatic
   allocation is already right; the rule was solving a ten-column problem this
   table does not have. */

/* ---------------------------------------------------------------------------
   OWNERS: THE PEOPLE ON SUBSCRIPTIONS (admin_owners.php).

   Ten columns in two blocks - the PERSON, then their MEMBERSHIP of one account
   - so the same hairline the Subscriptions table uses marks the seam, and the
   same declared widths keep a one-word "Seat" from taking more room than a
   Malaysian full name. Sized from the widest real content:
     Person 15%    "Datin Sarah Tan binti Abdullah" without breaking a word
     Email 15%     the column with no natural ceiling, so it wraps not clips
     Subscription 15%   a holding company name of five or six words
     Actions 10%   one Manage button, measured
   ------------------------------------------------------------------------ */
.tbl-people { table-layout: fixed; min-width: 1040px; }
.tbl-people thead th { white-space: normal; }
.tbl-people th:nth-child(1)  { width: 15%; }   /* Person          */
.tbl-people th:nth-child(2)  { width: 15%; }   /* Email           */
.tbl-people th:nth-child(3)  { width: 13%; }   /* Their law firm  */
.tbl-people th:nth-child(4)  { width:  8%; }   /* Seat            */
.tbl-people th:nth-child(5)  { width:  8%; }   /* Login           */
.tbl-people th:nth-child(6)  { width: 15%; }   /* Subscription    */
.tbl-people th:nth-child(7)  { width:  9%; }   /* On the account  */
.tbl-people th:nth-child(8)  { width:  9%; }   /* Membership      */
.tbl-people th:nth-child(9)  { width:  8%; }   /* Since           */
.tbl-people th:nth-child(10) { width: 10%; }   /* Actions         */
.tbl-people td { overflow-wrap: anywhere; }

@media (max-width: 760px) {
  .tbl-people.is-stacked { table-layout: auto; min-width: 0; }
}

/* ---------------------------------------------------------------------------
   THE SUBSCRIPTION REGISTER, INSIDE THE DIALOG.

   Six columns in a 920px dialog that is 100vw - 32px on a phone. The widths are
   declared for the same reason .tbl-owners declares its: negotiated widths give
   the date range - the only column anybody reads across - whatever is left over
   after five short ones have taken what they liked.

   NO min-width FLOOR. Every other list in this product sits in a page-width
   scroller; this one sits inside a dialog, and a horizontal scrollbar inside a
   modal is a control people do not find. Below 760px it stacks like every other
   .tbl-stack instead, which is why the markup carries that class.
   ------------------------------------------------------------------------ */
.tbl-subs { table-layout: fixed; width: 100%; }
.tbl-subs thead th { white-space: normal; }
.tbl-subs td, .tbl-subs th { padding-left: 8px; padding-right: 8px; }
.tbl-subs th:nth-child(1) { width: 32%; }   /* Period          */
.tbl-subs th:nth-child(2) { width:  9%; }   /* Days            */
.tbl-subs th:nth-child(3) { width: 15%; }   /* Grace           */
.tbl-subs th:nth-child(4) { width: 16%; }   /* State           */
.tbl-subs th:nth-child(5) { width: 18%; }   /* Added           */
/* One 28px button and its cell padding. */
.tbl-subs th:nth-child(6) { width: 10%; }   /* Remove          */
.tbl-subs td { overflow-wrap: anywhere; }

/* THE PERIOD IN FORCE, marked on the row rather than only in its State cell.
   A register of eight renewals is eight visually identical lines and the one
   that matters is one of them; a reader scanning for it should not have to read
   a column to find it. Same --brand-tint the "you are inside this firm" row
   uses on the Owners table, plus a left edge so the mark survives a grayscale
   print and a colour-vision difference. The word "Current" stays in the State
   cell: colour never carries a state on its own (Gate A). */
.tbl-subs tr.is-current > td { background: var(--brand-tint); }
.tbl-subs tr.is-current > td:first-child { box-shadow: inset 3px 0 0 var(--brand-2); }

/* Stacked, the row IS the card, so the accent moves to its edge - a shadow on
   the first cell would sit around the Period line only and read as a highlight
   on one field. */
@media (max-width: 760px) {
  .tbl-subs.is-stacked tr.is-current { box-shadow: inset 3px 0 0 var(--brand-2); }
  .tbl-subs.is-stacked tr.is-current > td { background: transparent; }
}

/* The add form, once revealed, is a task and not a footnote: it gets the
   section rule .subcard already draws plus a little more air above it, because
   it arrives under a table and needs to read as a different kind of thing. */
.ls-dialog .subcard[data-reveal-panel] { padding-top: 14px; }

/* ---------------------------------------------------------------------------
   THE OWNERS TABLE ON A PHONE.

   Three things the generic stacking rules get wrong on this particular column
   set, all of them caused by the first column being a row NUMBER.
   ------------------------------------------------------------------------ */
@media (max-width: 760px) {
  /* 1. A ROW NUMBER IS NOT A HEADING. The stacking rules give the first cell
        the block's title treatment, so every record was headed "1", "2", "3"
        and the person's name sat below as an ordinary labelled field. The
        number is a scanning aid for a wide grid and means nothing in a list of
        blocks, so it goes, and the name takes the heading it should have had.

        BY CLASS, NOT BY POSITION, since the rowspan arrived: on a continuation
        row `:first-child` is the Law Firm Name cell, and this rule would have
        HIDDEN the practice - the one fact those rows exist to carry - on every
        phone, with nothing to show anything was missing. */
  /* `td.c-no`, NOT `.c-no`. The shared rule it has to beat is
     `.tbl-stack.is-stacked td:first-child`, and :first-child counts as a class:
     that is (0,3,1), while a bare `.tbl-owners.is-stacked .c-no` is (0,3,0) and
     LOSES. Measured, not reasoned about after the fact - the row number was
     printed as the heading of every card on a 390px screen. Naming the element
     makes it (0,3,1), and this block sits later in the file. */
  .tbl-owners.is-stacked td.c-no { display: none; }
  .tbl-owners.is-stacked td.c-name {
    display: block;
    font-size: 15px;
    padding: 0 0 8px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--line-soft);
  }
  .tbl-owners.is-stacked td.c-name::before { content: none; }

  /* 1b. A GROUP IS ONE CARD WITH ITS PRACTICES UNDER IT.
        Stacked, every <tr> is its own bordered block, so an owner holding three
        practices became three cards and two of them had no name on them at all.
        The person's card and its continuations are joined into one: square
        edges where they meet, no gap between them, and a dashed rule to say
        where one practice ends and the next begins.

        The continuation rows also give their first cell back its ordinary
        labelled grid - the shared `td:first-child` rule would otherwise print
        the practice as a heading in one card and as "LAW FIRM NAME  x" in the
        next, which is two treatments for one column. */
  .tbl-owners.is-stacked tr.has-more {
    margin-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    border-bottom: 0;
  }
  .tbl-owners.is-stacked tr.is-more {
    border-top: 1px dashed var(--line);
    border-top-left-radius: 0;
    border-top-right-radius: 0;
  }
  .tbl-owners.is-stacked tr.is-more td:first-child {
    display: grid;
    font-size: inherit;
    padding: 3px 0;
    margin-bottom: 0;
    border-bottom: 0;
  }
  .tbl-owners.is-stacked tr.is-more td:first-child::before { content: attr(data-label); }

  /* 2. A DATE RANGE THAT WILL NOT WRAP OVERPRINTS ITS OWN LABEL.
        `.col-term` is nowrap so "27/06/2025 to 27/06/2026" holds together in a
        14% column on a desktop. In the stacked grid the label column is 34%,
        the value is wider than what is left, and nowrap made it spill back
        across "SUBSCRIPTION PERIOD" instead of taking a second line. The
        reason for the rule is gone once the grid is gone. */
  .tbl-stack.is-stacked .col-term { white-space: normal; }

  /* 3. A SECOND ELEMENT IN A CELL used to be handled here, for this table
        only. It is a property of the shared stacking grid, not of the Owners
        screen, so it moved up to `EVERY CHILD OF A STACKED CELL BELONGS UNDER
        THE VALUE` beside the chip rule. Nothing table-specific is left. */
}

/* ===========================================================================
   A TABBED RECORD DIALOG KEEPS ONE HEIGHT. 27 Aug 2026.
   ===========================================================================

   Alex, of the Owner Console's firm record: "redesign this modal, so same
   height."

   `.ls-dialog` is `height: fit-content; max-height: 93dvh` and that is right
   for a form: a dialog asking three questions has no business being as tall as
   one asking thirty. It is wrong for a RECORD, whose length is whatever the
   practice happens to have - eleven facts and no outstanding setup, or a
   register of forty periods - so the frame moved with the content and the
   footer moved with it.

   IT WAS WRITTEN FOR TABS AND IT OUTLIVED THEM (28 Aug 2026). The three panels
   are one panel now; the argument for a fixed frame is unchanged, because the
   variable it was protecting against was never the tab, it was the length of
   what the tab held.

   A DEFINITE HEIGHT, AND THE BODY SCROLLS - which is this application's modal
   anatomy anyway, and already works underneath: `.ls-dialog[open]` is a flex
   column, `.dialog-body` is `flex: auto; min-height: 0; overflow: hidden auto`,
   and the heading and `.dialog-actions` are `flex: none`. Nothing new is
   needed but the number.

   720px, AND WHY THAT NUMBER. It is what the firm record already measured when
   this shipped, so nothing on screen jumped that day; it clears the 700px-tall
   content area of a 13-inch laptop at 1280x800 through the 93dvh cap; and it
   leaves a little over 550px of body under a fixed heading and footer, which
   holds the whole practice - registration, tax, indemnity, address, numbering,
   appointments, the firm's year - in two columns before anything scrolls.

   IT SURVIVED THE MERGE, AND THE NUMBER DID NOT CHANGE. When the record was
   three tabs this was sized to the longest of them; now it is one panel that
   always scrolls, and 720 is still right for the same two reasons - the laptop
   cap above it, and enough body to read the practice without moving. Measured
   again on 28 Aug 2026: 720px frame, 557px body.

   SPECIFICITY IS DELIBERATE. `.ls-dialog:not(.assistant):not(.record)` earlier
   in this file is (0,3,0) and sets fit-content; this is (0,4,0) and beats it
   without touching that rule, which is still right for every dialog that is
   not a record.

   OPT-IN, NEVER AUTOMATIC. A dialog gets this by carrying `tabbed` in its
   class list, so a one-question dialog that happens to hold a segmented control
   is not silently stretched to 720px of white space.
   ------------------------------------------------------------------------ */
/* Height comes from the one MODAL HEIGHT rule at the end of this file. */

/* TWO COLUMNS OF FACTS INSIDE A DIALOG BODY.

   One eleven-row definition list reads as eleven identically weighted lines
   with no shape: the reader counts down it to find a TIN. Split into four short
   headed groups it has shape, but stacked in one column it is then twice as
   tall as the frame above allows. In two columns at `wide`, the whole tab fits
   without scrolling - which is what makes the fixed height a frame rather than
   a scrollbar.

   Below 760px it is one column, where `.defs` itself already stacks its label
   over its value, and the four groups simply follow one another. */
.dlg-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 28px;
  align-items: start;
}
@media (max-width: 760px) {
  .dlg-cols { grid-template-columns: minmax(0, 1fr); gap: 0; }
}

/* THE STATE PILLS IN A DIALOG'S SUBHEADING. Not a new component: the same
   `.pill` the tables use, given a little air from the sentence it follows and
   allowed to wrap as a group rather than one pill at a time. */
.dlg-tags { display: inline-flex; flex-wrap: wrap; gap: 6px; margin-left: 6px; vertical-align: baseline; }

/* THE FOOTER WRAPS. A firm record can offer Close, Add Subscription, Firm
   Management and Open Firm at once - four, to a LawServa engineer holding a
   seat at the practice. (It was five until Move Firm was removed on 28 Aug
   2026.) A row that cannot wrap pushes a button off the edge of the dialog
   rather than onto a second line. Below 760px `.dialog-actions` is already a
   full-width column and this changes nothing there. */
.dialog-actions { flex-wrap: wrap; }

/* NO FOOTER BUTTON IS TIED TO A PANEL ANY MORE. 28 Aug 2026.

   `[data-for-tab]` and the `@supports selector(:has(*))` block that read it
   showed Move Firm and Save Subscription only while the Subscription tab was
   open, because the form they submit lived in a panel the other two tabs hid.
   The record is one panel now and Move Firm is gone, so the form is always on
   screen above its button and there is nothing left to hide. */

/* THE REGISTER WITHOUT ITS ACTION COLUMN.

   The Owner Console renders the same table as the Admin Console minus the
   Remove button, because removing a period is LawServa's act and not the
   customer's. Five columns, not six, so the six declared widths above would
   leave the last one paying for a column that is not there. Declared rather
   than left to `table-layout: fixed` to rescale, so the date range - the only
   column anybody reads across - keeps the room it needs. */
.tbl-subs.readonly th:nth-child(1) { width: 36%; }  /* Period */
.tbl-subs.readonly th:nth-child(2) { width: 10%; }  /* Days   */
.tbl-subs.readonly th:nth-child(3) { width: 17%; }  /* Grace  */
.tbl-subs.readonly th:nth-child(4) { width: 17%; }  /* State  */
.tbl-subs.readonly th:nth-child(5) { width: 20%; }  /* Added  */

/* THE FOUR GROUPS IN A TWO-COLUMN DIALOG SHARE ONE LABEL COLUMN.

   `.defs` sizes its label column to `max-content`, which is right for one list
   and wrong for four side by side: each group measures its own longest label, so
   "ADDRESS" and "MATTER NUMBERS" put their values at different x positions in
   the same visual column and the tab reads as four separate little tables
   rather than one page of facts. A fixed width lines all four up.

   126px is measured in the browser, not chosen. The longest labels in the
   registration and tax groups are "BAR COUNCIL ROLL" at 121px and "MATTER
   NUMBERS" at 115px, so nothing there wraps. Two labels further down the record
   are longer - "ACCOUNTANT'S REPORT" at 152px and "COMPLIANCE OFFICER" at
   139px - and those take a second line on purpose: sizing the column to the
   longest label in the application would spend 152px of a 420px column on grey
   small caps and push every value in the record to the right to do it.

   (Written when the record was three tabs. It is one panel since 28 Aug 2026;
   the measurements and the reasoning are unchanged, only the word "tab".)

   ONLY ABOVE 640px. Below it, `.defs` stacks its label over its value and there
   is no label column left to align - and this rule would reinstate one. The
   media query is what keeps that from happening, not luck: the stacking rule
   sits earlier in the file at equal specificity, so an unguarded rule here
   would beat it. */
@media (min-width: 641px) {
  .dlg-cols .defs { grid-template-columns: 126px minmax(0, 1fr); }
}

/* A RECORD'S FOOTER GOES TWO-UP ON A PHONE, NOT FOUR DEEP.

   Every dialog in this application stacks its footer into one full-width column
   below 760px, which is right when there are two buttons and wrong when there
   are five. Measured at 375x812 on the firm record when it still had tabs:
   Close, Move Firm, Add Subscription and Open Firm took 380px of a 720px
   dialog, leaving the body 330px - the banner and half a table row - and the
   register was cut off mid-line by the footer. The fixed height did not cause
   that; it made it impossible to hide by growing the frame.

   The record is one panel and Move Firm is gone (28 Aug 2026), so the worst
   case is four buttons rather than five - and four still needs this rule.

   Two per row halves it. `white-space: normal` comes with it, because
   "Save Subscription" does not fit 156px on one line and `.btn` is nowrap
   everywhere else; a label on two lines is a button that reads, and a label
   clipped by its own box is not. Both stay well above the 44px coarse-pointer
   floor either way.

   SCOPED TO `.tabbed`, deliberately. The one-column footer is the right answer
   for the ordinary two-button dialog and is not touched. A tabbed record is
   precisely the dialog that accumulates five actions, and it is the only one
   asking for this.

   900px, NOT 760: it overrides the shared column-reverse rule and has to cover
   the same range, or a 768px tablet - which is not a phone and does not get the
   stacked table - keeps the four-deep footer. Measured there before this
   changed: 235px of footer over a 388px body. */
@media (max-width: 900px) {
  .ls-dialog.tabbed .dialog-actions {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: stretch;
  }
  .ls-dialog.tabbed .dialog-actions > * {
    flex: 1 1 calc(50% - 5px);
    width: auto;
    min-width: 0;
  }
  /* Two of them are a <form> wrapping the button, because entering a firm is a
     POST. The form is the flex item; the button has to fill it. */
  .ls-dialog.tabbed .dialog-actions > form { display: flex; }
  .ls-dialog.tabbed .dialog-actions .btn {
    width: 100%;
    white-space: normal;
  }
}

/* The record's three-cell tab strip used to need a phone rule of its own here
   (`.modaltabs > nav.segmented`, kept as a row between 361 and 900px so that
   "Firm", "Setup" and "Subscription" did not stack into 276px of a 528px
   dialog body). The record is one panel now, there is no strip, and the rule
   is deleted with the markup that carried it. `.segmented` keeps the shared
   stacked behaviour, which is what the ledger's filter wanted all along. */

/* THE REGISTER KEEPS ITS OWN WIDTH INSIDE A DIALOG, AND THIS RESTORES AN INTENT
   THAT WAS ALREADY WRITTEN DOWN.

   `.tbl-subs` says in its own comment, above: no min-width floor, because it
   lives inside a modal and a horizontal scrollbar inside a modal is a control
   people do not find. It never declared one - but it never needed to, because
   the shared floor is `.table-scroll table:has(thead th:nth-child(6))` and the
   Owner Console's copy of this table had five columns.

   Giving that console the Remove column made it six, and at 768px the dialog is
   736px wide against a 720px floor plus padding: the register began scrolling
   sideways inside the dialog. Measured, not guessed. The Admin Console's copy
   has had six columns all along and does the same thing at that width; both are
   fixed here, in the component, rather than twice in two screens.

   (0,3,1) against the floor's (0,2,3) - one more class, which is what decides
   it. The percentage widths above and `table-layout: fixed` do the rest: at
   690px the date range column is 221px and a date range sets at 160px. */
.table-scroll table.tbl-stack.tbl-subs { min-width: 0; }

/* THE COUNT ON A FILTER CHIP.
   Added 28 Aug 2026 with the Bug Reports / Feature Requests split, where the
   chips ARE the queue: "how many are waiting on me" is the question the person
   opened the screen to ask, and reading it off the strip beats pressing each
   chip to find out. page_filters() prints one only where the screen passed
   counts, and never where the number is zero.

   Sized off the chip's own font rather than a fixed px so it stays aligned
   when .subnav a drops to 8px padding under 700px. `warn` marks the two states
   where a human owes an answer - the same pair Feedback::statusTone() weights
   in the row - and it is a WEIGHT on a word that already says what it means,
   never the meaning itself: the chip reads "Waiting" whether or not the number
   is coloured, which is the rule every pill in this stylesheet follows. */
.subnav a .chipcount {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  min-width: 18px;
  border-radius: 9px;
  background: var(--line-soft);
  color: var(--ink-2);
  font-size: .85em;
  font-weight: 650;
  line-height: 18px;
  text-align: center;
  vertical-align: baseline;
}
.subnav a[aria-current="page"] .chipcount { background: var(--s3); color: var(--brand); }
.subnav a .chipcount.warn { background: var(--warn-bg); color: var(--warn); }
.subnav a[aria-current="page"] .chipcount.warn { background: var(--warn-bg); color: var(--warn); }

/* A pill carrying a SENTENCE rather than a one-word status. .pill is
   white-space:nowrap, which is right for "Draft" or "Today" and wrong for
   "Not a working day: Weekend in Wilayah Persekutuan Kuala Lumpur" - that one
   measured 402px and hung 68px off the side of a 375px phone in the calendar's
   week and agenda views, clipped, with nothing able to scroll to it.
   Added 29 Aug 2026. */
.pill.pill-sentence { white-space: normal; max-width: 100%; }

/* ---------------------------------------------------------------------------
   THE TWO FEEDBACK QUEUES AS DIALOGS (31 Aug 2026)

   They moved out of the left rail into the account menu and open as modals,
   one per lane. Everything below is either the dialog's own size or the small
   number of places where a control that used to be a LINK on a page is now a
   BUTTON in a dialog - because a link would navigate out of the modal, which
   is the whole fault this change fixes. The look is deliberately unchanged:
   the same .subnav strip, the same .toolbar, the same table.
   --------------------------------------------------------------------------- */

/* A QUEUE NEEDS HEIGHT, and a dialog that sizes to its content gives a short
   list a short box and a long list the whole viewport - so the strip and the
   footer move every time the filter changes. Fixed at the viewport instead:
   the list scrolls inside the body, the chips and the actions never move.
   .ls-dialog already supplies the three-row flex column, the body-only scroll
   and the min-height:0 that makes it work. */
/* Height comes from the one MODAL HEIGHT rule at the end of this file. */
.ls-dialog.fb-lane .dialog-body { display: flex; flex-direction: column; }

/* The search row does not scroll away with the list under it: a filter you
   cannot see is a filter you forget is applied. */
.ls-dialog.fb-lane .fb-tools { flex: 0 0 auto; margin-bottom: 12px; }
.ls-dialog.fb-lane .fb-queue  { flex: 1 1 auto; min-height: 0; }

/* Enough room that the loading line, an empty state and a full list do not
   each give the dialog a different shape as they replace one another. */
.ls-dialog.fb-lane .fb-queue { min-height: 220px; }
.ls-dialog.fb-lane .fb-loading { padding: 28px 2px; }

/* The chips. .subnav styles anchors, because everywhere else a chip is a link
   to a slice of a page. In here it re-fetches, so it is a button and needs the
   same six declarations spelled for a button element. Same tokens, same
   metrics - a reader must not be able to tell the two strips apart. */
.subnav.fb-chips button {
  border-radius: var(--radius-s) var(--radius-s) 0 0;
  color: var(--ink-2);
  background: none;
  border: 1px solid transparent;
  border-bottom: none;
  margin-bottom: -1px;
  padding: 8px 14px;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
}
.subnav.fb-chips button:hover { background: var(--line-soft); color: var(--ink); }
.subnav.fb-chips button[aria-pressed="true"] {
  background: var(--card);
  color: var(--brand);
  border-color: var(--line);
  border-bottom: 1px solid var(--card);
  font-weight: 650;
}
.subnav.fb-chips button:focus-visible { outline: 2px solid var(--brand-2); outline-offset: -2px; }

/* AND THE COUNT ON IT. The four .chipcount rules above are written for
   `.subnav a .chipcount`, so a chip that is a BUTTON got none of them: no
   margin, no padding, no pill - "Open3" run together as one word. Scoped the
   same way rather than loosened to `.subnav .chipcount`, because widening a
   selector to catch a new case silently restyles every existing one. */
.subnav.fb-chips button .chipcount {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  min-width: 18px;
  border-radius: 9px;
  background: var(--line-soft);
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 650;
  line-height: 18px;
  text-align: center;
}
.subnav.fb-chips button[aria-pressed="true"] .chipcount { background: var(--s3); color: var(--brand); }
.subnav.fb-chips button .chipcount.warn { background: var(--warn-bg); color: var(--warn); }
.subnav.fb-chips button[aria-pressed="true"] .chipcount.warn { background: var(--warn-bg); color: var(--warn); }

/* The account menu's two queue rows carry the unread count. .navcount pushes
   itself right with margin-left:auto, which needs a flex row to push against -
   the menu's other rows are display:block and have nothing to align. */
.usermenu-pop button { display: flex; align-items: center; gap: 8px; text-align: left; }

/* IS THERE ANYTHING NEW, WITHOUT OPENING THE MENU. A dot, not a number: out
   here it could only be a total across two queues, which is a figure with no
   use. The number is on the row it belongs to. */
.usermenu-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--accent, var(--brand));
  flex: 0 0 auto;
}

/* ===========================================================================
   MODAL HEIGHT - THE ONE RULE. 31 Aug 2026.

   Alex: "fix the bug report modal and also the feature request modal to 95vh,
   fixed them, memorize this, fix height for all modals, do not change height
   according to modal content."

   Every modal in the application is 95% of the viewport, always, whatever is
   in it. A modal does not grow as a list is filtered, does not jump when a
   validation message appears, and its footer - Close on the left, the action
   on the right - is in the same place on every screen and stays there while
   you use it. That predictability is the whole point and it is worth the empty
   space a short dialog now carries.

   DO NOT ADD ANOTHER HEIGHT DECLARATION FOR .ls-dialog ANYWHERE. Five of them
   accumulated between 14 and 31 August and the one that won was not the last
   in the file, it was the one with a :not() in it - so every attempt to change
   the height by editing one of the others silently did nothing. If a modal
   needs a different height, change THIS rule.

   dvh, NOT vh. `vh` on a phone is the viewport WITHOUT the browser's own
   chrome subtracted, so 95vh there is taller than the screen and pushes the
   footer - the row with the action in it - under the address bar. `dvh` is the
   same 95% of what is actually visible. Both lines are written: a browser too
   old for dvh keeps the vh value, which is the intent, and every current one
   takes the second.

   THE ONE EXCLUSION IS THE GENERATED CONFIRMATION (.ls-prompt, #dlg-confirm),
   and it is stated rather than silent. It is one sentence and two buttons -
   "Are you sure?" - and at 95% of the viewport the question sits at the top
   and the answer is marooned at the bottom edge, half a screen apart, with the
   destructive button furthest from the question it belongs to. It is a prompt
   rather than a window, it holds no content that can vary, so there is nothing
   for a fixed height to stabilise and a real cost to imposing one. Tell me and
   it goes in with the rest.
   ======================================================================== */
.ls-dialog:not(.ls-prompt) {
  height: 95vh;
  height: 95dvh;
  max-height: 95dvh;
}

/* The prompt keeps the behaviour every modal had until today: as tall as the
   question, never taller than the screen. fit-content rather than auto -
   a modal is laid out with inset:0 and margin:auto, and `auto` is an unknown
   in an over-constrained equation that CSS resolves by filling the box, so it
   would silently be 95dvh anyway. Measured 14 Aug 2026. */
.ls-dialog.ls-prompt {
  height: -webkit-fit-content;
  height: fit-content;
  max-height: 95dvh;
}

/* ---------------------------------------------------------------------------
   THE ADMIN CONSOLE'S LAW FIRMS TABLE. 31 Aug 2026.

   The console split into two screens - Owners is one row per person, this is
   one row per practice - and the second table inherited none of the .tbl-owners
   rules because it is not that table. Only the ones that are genuinely about
   BOTH are repeated here, and they are repeated rather than the selector being
   widened to `.tbl-owners, .tbl-firms`: those two are different shapes now and
   a shared selector is how a change meant for one silently lands on the other.
   --------------------------------------------------------------------------- */
.tbl-firms { min-width: 900px; }
.tbl-firms thead th { white-space: normal; vertical-align: bottom; }
.tbl-firms td, .tbl-firms th { padding-left: 10px; padding-right: 10px; vertical-align: top; }

/* The counter is a label, not a measurement: right-aligned in a column sized
   for the header word it sits under, it drifted a long way from the row it
   numbers. Narrow and left. */
.tbl-firms .c-no { width: 1%; white-space: nowrap; text-align: left; }
.tbl-firms .c-firm { min-width: 190px; }

/* The icons stay on one line. Wrapped, Attach sat above Enter and the cell grew
   to two rows for two buttons - and a control that moves down a line when a
   neighbour appears is the thing the fixed modal height was fixing elsewhere. */
.tbl-firms .row-actions .acts { flex-wrap: nowrap; }

/* The period is the door to the register behind it - the same treatment the
   Owners table gave this cell before the split, moved with the column. */
.tbl-firms .col-term .linkbtn {
  color: inherit;
  font-weight: inherit;
  text-decoration: underline dotted var(--line);
  text-underline-offset: 3px;
}
.tbl-firms .col-term .linkbtn:hover,
.tbl-firms .col-term .linkbtn:focus-visible {
  color: var(--brand-2);
  text-decoration: underline solid currentColor;
}
.tbl-firms tbody > tr.is-inside > td { background: var(--brand-tint); }
.tbl-firms.is-stacked td { white-space: normal; }

/* ---------------------------------------------------------------------------
   THE ACCOUNT LINE ON THE OWNERS TABLE. 31 Aug 2026.

   A person's row lists the accounts they administer, one line each, and each
   line carries its own two controls - Edit and Close - because those act on the
   ACCOUNT and the row's action column is about the PERSON. They are drawn at
   the grain they act at, which is the rule the whole console split follows.

   .xsmall, not .small: these sit inline with a line of text rather than in an
   action column, and a 28px button beside 14px type makes the line the button
   is attached to hard to read. Tap target is preserved on coarse pointers by
   the 44px floor further down this file.
   --------------------------------------------------------------------------- */
.acct-line {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.acct-line + .acct-line { margin-top: 6px; }
.acct-line form { display: inline-flex; margin: 0; }

/* The controls stay quiet until the line is wanted: an account list with two
   loud buttons per line reads as a toolbar, not as a list of accounts. They are
   always in the DOM and always reachable by keyboard - this is opacity, never
   display:none, so nothing is hidden from a screen reader or from Tab. */
.acct-line .btn.icon.xsmall {
  width: 26px;
  height: 26px;
  padding: 0;
  opacity: .55;
  transition: opacity .12s ease;
}
.acct-line:hover .btn.icon.xsmall,
.acct-line .btn.icon.xsmall:focus-visible { opacity: 1; }
.acct-line .btn.icon.xsmall svg { width: 14px; height: 14px; }

@media (pointer: coarse) {
  /* No hover on a touch screen, so nothing may depend on it. */
  .acct-line .btn.icon.xsmall { opacity: 1; min-width: 44px; min-height: 44px; }
}

/* ===========================================================================
   THE CALENDAR, REDRAWN. 31 Aug 2026.
   ===========================================================================

   The complaint was density, and density on this screen is not a taste
   question. A practice calendar is read in two postures: a partner at seven in
   the evening asking "is anything going to go wrong tomorrow", and a fee earner
   at nine asking "where do I have to be". Both are answered by SEEING THE
   PERIOD AT ONCE. Anything that pushes the grid down the page, or spends a
   cell's height on chrome, is taking away the only thing the screen does.

   Three measurements drove this, taken at 1440x900 on the demo practice:
     - the month grid started at y=874, so on a 900px viewport the month was
       one row of cells tall;
     - every cell spent 17px on a counts row stacked under the day number,
       which is a chip and a half out of a 118px box;
     - one hearing in the day view occupied five stacked paragraphs, so six
       hearings was a screen and a half to read six titles.

   Nothing here hides anything. No control became a menu, no fact became a
   tooltip, the tab order is unchanged, and the bar is ordinary normal-flow
   markup that scrolls away with the page. Everything below is either a smaller
   number where a number was too big, or two things put on one line that were
   never two lines' worth of meaning.
   ------------------------------------------------------------------------ */

/* --- 1. the command bar --------------------------------------------------- */

/* One card, two rows: WHEN on the first, WHICH on the second. It replaced a
   view nav, a stepper block and a search toolbar that were three full-width
   bands with three margins between them - 214px of chrome to ask one question
   in three parts. */
.cal-cmd {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 12px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
}
.cal-cmd-top {
  display: flex;
  align-items: center;
  gap: 10px 14px;
  flex-wrap: wrap;
}

/* Back, Today and Next are ONE control and are drawn as one: a segmented group
   with hairlines between, not three buttons with gaps that read as three
   unrelated decisions. The arrows are glyphs because "Back" and "Next" beside a
   month name are the two least ambiguous controls in any calendar ever made,
   and both carry a real aria-label. */
.cal-step {
  display: inline-flex;
  align-items: stretch;
  flex: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--card);
}
.cal-step > * {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 30px;
  padding: 0 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: none;
  border-left: 1px solid var(--line);
}
.cal-step > :first-child { border-left: 0; }
.cal-step-arrow { font-size: 17px; line-height: 1; padding: 0 11px; }
.cal-step a:hover { background: var(--line-soft); color: var(--ink); }
.cal-step a:focus-visible { outline: 2px solid var(--brand-2); outline-offset: -2px; }
/* Already on today. Present, quiet, and plainly not a control - a toolbar whose
   buttons come and go is worse than one with a button that has nothing to do. */
.cal-step-today.is-here { color: var(--muted); background: var(--line-soft); cursor: default; }

/* The period is the largest thing in the bar because it is the answer to
   "where am I". It is an h2: the grid below it is this heading's content. */
.cal-period {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -.2px;
  color: var(--ink);
}

/* The four views, as a segmented switch. They are a FILTER on one screen - the
   same dates in four shapes - which is why they are not tabs. */
.cal-views {
  display: inline-flex;
  flex: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--bg);
}
.cal-views a {
  padding: 6px 13px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: none;
  border-left: 1px solid var(--line);
  min-height: 30px;
  display: inline-flex;
  align-items: center;
}
.cal-views a:first-child { border-left: 0; }
.cal-views a:hover { background: var(--line-soft); color: var(--ink); }
.cal-views a:focus-visible { outline: 2px solid var(--brand-2); outline-offset: -2px; }
/* Colour is never the only channel: the current view is also the only one with
   a filled ground and a weight step. */
.cal-views a[aria-current="page"] {
  background: var(--brand-2);
  color: #fff;
  font-weight: 700;
}

/* The filters, on their own line, at a height that says "secondary". */
.cal-cmd-filters {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0;
  padding: 8px 0 0;
  border-top: 1px solid var(--line-soft);
}
.cal-cmd-filters .field { margin: 0; display: flex; }
.cal-cmd-filters .field.grow { flex: 1 1 240px; min-width: 180px; }
.cal-cmd-filters input[type="search"],
.cal-cmd-filters select {
  width: 100%;
  min-height: 32px;
  padding: 5px 9px;
  font-size: 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--card);
  color: var(--ink);
}
.cal-cmd-filters .btn { min-height: 32px; padding: 5px 12px; font-size: 13px; }
.cal-clear { font-size: 12.5px; color: var(--brand-2); text-decoration: underline; }

/* The scope ribbon: one quiet line directly under the bar, not a boxed notice.
   It states a fact about what is on screen; it is not an alert. */
.cal-ribbon {
  margin: -4px 0 12px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}
.cal-ribbon strong { color: var(--ink-2); }
.cal-ribbon .btn { margin-left: 6px; padding: 2px 9px; font-size: 12px; min-height: 24px; }

/* --- 2. the month grid ---------------------------------------------------- */

/* 118px to 102px. The cell head below gave back a whole row, so the cell holds
   FOUR entries in less height than it held three. It still never scrolls. */
.cal-month td { height: 102px; }
.cal-month th {
  padding: 6px 7px;
  font-size: 11px;
  letter-spacing: .6px;
}
.cal-cell { padding: 5px 6px; }

/* THE CELL HEAD. Day number left, severity counts right, one line.
   The counts are what a partner reads first when scanning a month - severity
   before titles - so they sit at the top edge on the same baseline as the
   number rather than in a band of their own underneath it. */
.cal-cellhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  min-height: 16px;
}
.cal-daynum { font-size: 12.5px; line-height: 16px; }
.cal-counts { margin-top: 0; gap: 3px; justify-content: flex-end; flex-wrap: nowrap; }
.cal-count { font-size: 10px; padding: 1px 4px; }

/* Today is a filled marker on the number, not an outline round the cell. The
   outline was competing with the focus ring for the same 2px, so the day you
   were on and the day you had tabbed to looked alike. */
.cal-month td.is-today { outline: 0; box-shadow: inset 0 0 0 1px var(--brand-2); }
.cal-month td.is-today .cal-daynum {
  background: var(--brand-2);
  color: #fff;
  border-radius: 999px;
  padding: 0 6px;
  min-width: 20px;
  text-align: center;
}
.cal-cell:focus-visible { outline: 3px solid var(--brand-2); outline-offset: -3px; }

.cal-closed-name { font-size: 10px; margin-top: 0; }

/* The chip, tightened. The 3px severity rule down the left is the signature of
   this screen and is untouched: it is the shape, not the colour, and it is what
   makes the grid readable to a reader who sees no colour at all. */
.cal-chip {
  margin-top: 2px;
  padding: 1px 4px 1px 5px;
  font-size: 11px;
  line-height: 1.35;
  gap: 4px;
}
.cal-chip .cal-glyph { width: 9px; font-size: 10px; }
.cal-more { margin-top: 3px; font-size: 10.5px; }

/* --- 3. the event, in the day, week, agenda and day dialog ---------------- */

/* The heading rule the inline style used to carry, now where it belongs and
   applied at every level the caller may legitimately pass. */
.cal-event :is(h2, h3, h4, h5, h6) {
  font-size: 14px;
  line-height: 1.35;
  margin: 0 0 3px;
  font-weight: 600;
}
.cal-daylist > li { padding: 8px 2px; }
.cal-event { gap: 9px; }
.cal-when { min-width: 46px; font-size: 12.5px; padding-top: 1px; }

/* The pills, on one line, tight enough to read as a single statement about the
   date rather than as four separate badges. */
.cal-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0 0 3px;
}
.cal-pills .pill { font-size: 11px; padding: 1px 7px; }

/* The facts line. One wrapping line, middot separated, each value in the
   stronger ink and each label in the quieter one - so the eye lands on
   "Sesyen 4 KL" and "WA-B52-114-08/2026", which are what you actually need at
   the door, and reads the labels only if it has to. */
.cal-facts {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
}
.cal-facts strong { color: var(--ink-2); font-weight: 600; }
.cal-fact-k { color: var(--muted); }
.cal-fact-sub { color: var(--muted); }
.cal-fact-sep { padding: 0 6px; color: var(--line); }
.cal-note {
  margin: 3px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}
.cal-clash, .cal-landing { margin-top: 5px; padding: 5px 8px; }

/* --- 4. the apparatus under the grid -------------------------------------- */

/* Consulted once, or when something looks wrong. It is under the month for that
   reason, and it is drawn quietly for the same one. */
.cal-legend {
  gap: 4px 14px;
  padding: 8px 12px;
  font-size: 11.5px;
  margin-bottom: 12px;
}
/* REDESIGNED 14 Sep 2026: the apparatus is a section of its own under the
   grid, with a rule above it and air between its cards. Nothing in it is
   read before the month, so it is quiet; nothing in it is small, because
   small and crowded is what it was. */
.cal-apparatus {
  margin-top: 40px; padding-top: 28px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 20px;
  max-width: 1040px;
}
.cal-apparatus > * { margin: 0; }
.cal-app-h { font-size: 14px; font-weight: 600; color: var(--ink); margin: 0 0 14px; }
.cal-apparatus .cal-legend {
  display: block; padding: 20px 24px 22px; gap: 0; font-size: 13px; margin: 0;
}
.cal-legend-row { display: flex; flex-wrap: wrap; gap: 10px 28px; }
.cal-legend-row span { display: inline-flex; align-items: center; gap: 8px; font-weight: 500; }
.cal-apparatus .cal-legend .cal-swatch { width: 14px; height: 14px; border-radius: 4px; }
.cal-legend-rules {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px 28px;
  margin: 18px 0 0; padding: 16px 0 0; border-top: 1px solid var(--line-soft, var(--line));
}
.cal-legend-rules div { display: flex; flex-direction: column; gap: 2px; }
.cal-legend-rules dt { font-weight: 600; color: var(--ink); }
.cal-legend-rules dd { margin: 0; color: var(--muted); }
.cal-apparatus .cal-truth { padding: 20px 24px 22px; margin: 0; }
.cal-apparatus .cal-truth h2 { font-size: 14px; margin: 0 0 10px; }
.cal-apparatus .cal-truth p { font-size: 13.5px; line-height: 1.55; margin: 0 0 10px; max-width: 72ch; }
.cal-apparatus .cal-truth p:last-child { margin-bottom: 0; }
.cal-apparatus .cal-truth ul { margin: 4px 0 14px 20px; font-size: 13.5px; line-height: 1.5; display: grid; gap: 6px; }
.cal-sources {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card);
}
.cal-sources > summary {
  cursor: pointer; padding: 16px 24px; font-size: 14px; font-weight: 600; color: var(--ink);
  list-style: none; display: flex; align-items: center; gap: 10px;
}
.cal-sources > summary::-webkit-details-marker { display: none; }
.cal-sources > summary::before {
  content: ""; width: 7px; height: 7px; flex: none;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(-45deg); opacity: .55; transition: transform .12s ease-out;
}
.cal-sources[open] > summary::before { transform: rotate(45deg); }
.cal-sources-form { padding: 4px 24px 22px; }
.cal-sources-form .cal-state-grid { gap: 12px 24px; }
.cal-sources-apply { margin: 18px 0 0; }
.cal-app-note { font-size: 12.5px; line-height: 1.6; color: var(--muted); max-width: 72ch; padding: 0 4px; }

/* --- 5. small screens ----------------------------------------------------- */

@media (max-width: 900px) {
  /* The period keeps the first line to itself; the view switch takes the
     second, full width, so four targets share the width evenly instead of
     wrapping two-and-two. */
  .cal-views { width: 100%; }
  .cal-views a { flex: 1 1 0; justify-content: center; padding: 6px 4px; }
}

@media (max-width: 760px) {
  .cal-month td { height: auto; min-height: 68px; }
  .cal-period { font-size: 16px; }
  .cal-cmd { padding: 8px; }
  .cal-cmd-filters .field.grow { flex: 1 1 100%; }
  .cal-when { min-width: 42px; }
}

@media (pointer: coarse) {
  /* Density yields to the 44px floor, always. */
  .cal-step > *, .cal-views a { min-height: 44px; }
  .cal-cmd-filters input[type="search"], .cal-cmd-filters select,
  .cal-cmd-filters .btn { min-height: 44px; font-size: 14px; }
  .cal-chip { padding: 5px 5px 5px 6px; font-size: 12px; }
  .cal-daylist > li { padding: 12px 2px; }
}

@media print {
  /* The bar is chrome. The ribbon is not: a printed month showing one person's
     dates and not saying so is the same defect on paper, and worse, because
     paper is what gets handed across a desk. */
  .cal-cmd { display: none !important; }
  .cal-month td { height: auto; }
}

/* The day view's working-day strip, in place of a card head that printed the
   date a second time. Quiet, because on most days it is not there at all and a
   block that appears and disappears must not shout when it does. */
.cal-closed-strip {
  margin: 0 0 4px;
  padding: 6px 9px;
  font-size: 12.5px;
  border-left: 3px solid var(--sev-warning);
  background: var(--sev-warning-bg);
  color: var(--sev-warning);
  border-radius: 0 4px 4px 0;
}
/* The middot between facts was drawn in the hairline colour and disappeared,
   so "Court Sessions Court at Kuala Lumpur Cause no WA-..." ran together as one
   string. It is a separator; it has to be seen to separate. */
.cal-fact-sep { color: var(--muted); opacity: .6; }

/* ---------------------------------------------------------------------------
   THE MONTH ON A PHONE. 31 Aug 2026.

   It stays a seven-column grid, because that is what a month is and because the
   question a phone is asked is never "read me the titles" - it is "which days
   this month am I in trouble on". At 375px a cell is about 44px wide, which
   cannot hold a title and does not try: the cell carries the day number and the
   severity counts, both of which fit and both of which answer that question,
   and the whole cell is already a link into the day.

   The chips are hidden with display:none rather than clipped. Every one of them
   is also in the cell's aria-label, which is unchanged, so nothing is lost to a
   screen reader on a phone - and clipping half a word inside a 44px box is not
   information, it is the appearance of information.
   ------------------------------------------------------------------------ */
@media (max-width: 760px) {
  .cal-month { table-layout: fixed; width: 100%; }
  .cal-month th { padding: 5px 2px; font-size: 9.5px; letter-spacing: 0; text-align: center; }
  .cal-month td { height: auto; min-height: 52px; }
  .cal-cell { padding: 4px 3px; min-height: 52px; }
  .cal-cellhead { flex-direction: column; align-items: center; gap: 2px; }
  .cal-daynum { font-size: 13px; }
  .cal-counts { justify-content: center; flex-wrap: wrap; gap: 2px; }
  .cal-count { font-size: 9.5px; padding: 0 3px; }
  .cal-chip, .cal-more, .cal-closed-name { display: none; }
}

/* A dialog styles a bare <h3> as a SECTION LABEL - uppercase, letter-spaced,
   muted - which is right for "Client details" and wrong for the title of a
   hearing. Inside the day dialog every event heading is an h3, so ten court
   dates were shouted in grey capitals at the reader: "WITNESS STATEMENTS TO BE
   EXCHANGED". Measured 31 Aug 2026 on 12 September in the sandbox. An event
   title is a proper noun about a file, not a label on a group of fields, so it
   is stated in the ink and the case it was written in. */
.ls-dialog .cal-event :is(h2, h3, h4, h5, h6) {
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
}

/* ===========================================================================
   THE CALENDAR, REBUILT. 31 Aug 2026, replacing the same morning's redraw.
   ===========================================================================

   The redraw earlier today fixed the HEIGHT of this screen and left its
   LANGUAGE alone, and the language was the actual complaint. Audited against
   docs/design-protocol.md against a seeded 84-date September at 1440x1000,
   three findings, two of them structural:

   1. CLARITY, FAIL. 12 September printed four chips reading "Discovery
      affidavit", "Discovery affidavit", "Discovery affidavit", "Discovery
      affidavit". The cell spent its whole width on the one field that repeats
      and gave no time, no file and no court. Nothing about that is fixable by
      spacing.

   2. GATE 4.A, FAIL, "alarm design is fatigue design". EVERY chip at EVERY
      severity was painted on a tinted ground. At 84 dates roughly a third of
      the grid was filled colour, so one critical stamping window carried the
      same visual weight as sixteen routine attendances. A ramp on which
      everything is coloured is not a ramp. HIERARCHY and RESTRAINT fail with
      it, which is three anchors and the §5.3 rebuild trigger.

   3. The same fact was drawn twice inside one 157px cell: a row of coloured,
      bordered count pills at the top, and the coloured chips below saying the
      same thing in the same hues.

   WHAT THIS REBUILD ACTUALLY CHANGES: it stops painting everything and starts
   painting only the exceptional. Two states get a fill and no others - a
   CRITICAL date, and a date that is ALREADY PAST. Those are the two things a
   partner opens a month to find. Everything else is a hairline rule in its
   severity colour on the page's own ground, which keeps the non-colour channel
   the protocol requires (CR-4: the rule is the shape, the glyph is persistent,
   the legend is on the surface) while giving the two that matter somewhere to
   stand out FROM.

   Severity is still tokenized, still five-valued, still never colour alone.
   Nothing was deleted from the record: every fact the cell used to carry is
   still on the day, in the dialog, and in the cell's accessible name.
   ------------------------------------------------------------------------ */

/* --- 1. the head: two lines, separated by hairlines, not by a border -------- */

.cal-head {
  margin: 0 0 14px;
  padding: 0;
  border: 0;
  background: none;
  border-bottom: 1px solid var(--line);
}
.cal-head-row {
  display: flex;
  align-items: center;
  gap: 10px 16px;
  flex-wrap: wrap;
  padding: 0 0 10px;
}
.cal-head-row2 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  flex-wrap: wrap;
  padding: 0 0 10px;
  border-top: 1px solid var(--line-soft);
  padding-top: 10px;
}

/* The period is the answer to "where am I" and is now the largest thing on the
   line, one step under the h1. It was 17px competing with a 23px page title
   that says a word the rail and the tab strip both already say. */
.cal-head .cal-period {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 19px;
  font-weight: 650;
  letter-spacing: -.02em;
  color: var(--ink);
}

.cal-filters {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
  border: 0;
  flex: 1 1 420px;
}
.cal-filters .field { margin: 0; display: flex; }
.cal-filters .field.grow { flex: 1 1 220px; min-width: 160px; max-width: 460px; }
.cal-filters input[type="search"],
.cal-filters select {
  width: 100%;
  min-height: 32px;
  padding: 5px 9px;
  font-size: 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--card);
  color: var(--ink);
}
.cal-filters .btn { min-height: 32px; padding: 5px 12px; font-size: 13px; }

/* What came back, at the end of the line that asked for it.

   NAMED .cal-shown, NOT .cal-count. It was .cal-count for about ten minutes and
   that class already belongs to the little severity tallies inside a month
   cell, which carry a border and a padded ground - so this sentence rendered
   as a bordered box floating at the end of the filter row. A class name is an
   interface; reusing one that is already taken is the same defect as reusing a
   filename, and it fails silently and looks deliberate. */
.cal-shown {
  margin: 0;
  flex: 0 1 auto;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  text-align: right;
}
.cal-shown strong { color: var(--ink-2); }
.cal-shown .btn { margin-left: 6px; padding: 2px 9px; font-size: 12px; min-height: 24px; }

/* --- 2. the grid ---------------------------------------------------------- */

.cal-month th {
  padding: 5px 7px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .07em;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
}
.cal-month td {
  height: 104px;
  border-color: var(--line-soft);
}

/* A Sunday is never in doubt. A gazetted holiday moves, a state re-proclaims
   it, and an estimated Islamic date is not fixed until it is announced - so the
   two closures are drawn as two things. The weekend is a barely-there cool
   ground that shows the shape of the working week without competing with what
   is written on it; the holiday is warm and carries its own name. */
.cal-month td.is-closed  { background: #f8fafb; }
.cal-month td.is-holiday { background: #fdfaf3; }
.cal-month td.is-outside { background: var(--bg); }
.cal-month td.is-today   { outline: 0; box-shadow: inset 0 0 0 1.5px var(--brand-2); }

.cal-cell { padding: 5px 6px; }
.cal-cellhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  min-height: 17px;
}
/* An empty day and a seventeen-date day announced themselves identically. The
   number now carries the weight of the day it names. */
.cal-daynum { font-size: 12.5px; line-height: 17px; font-weight: 500; color: var(--muted); }
.cal-cell.has-dates .cal-daynum { font-weight: 700; color: var(--ink); }
.cal-month td.is-today .cal-daynum {
  background: var(--brand-2); color: #fff; border-radius: 999px;
  padding: 0 6px; min-width: 20px; text-align: center; font-weight: 700;
}
.cal-month td.is-outside .cal-daynum { color: #9aa7b5; font-weight: 500; }
.cal-closed-name {
  display: block; font-size: 10px; line-height: 1.25; color: var(--sev-warning);
  margin-top: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* THE COUNTS SUMMARISE WHAT IS NOT SHOWN, so they appear only on a cell that
   had to truncate. On a cell printing everything it holds, the entries are the
   summary and a second copy of them in another colour system is noise. They
   also lost their boxes: a glyph and a number in the severity ink says the same
   thing without putting five more filled rectangles on the grid. */
.cal-counts { display: none; margin: 0; gap: 7px; justify-content: flex-end; flex-wrap: nowrap; }
.cal-cell.is-truncated .cal-counts { display: flex; }
.cal-count-badge, .cal-counts .cal-count {
  font-size: 10.5px; font-weight: 700; line-height: 17px;
  padding: 0; border: 0; background: none; letter-spacing: .01em;
}
.cal-counts .cal-count.sev-critical { color: var(--sev-critical); background: none; border: 0; }
.cal-counts .cal-count.sev-warning  { color: var(--sev-warning);  background: none; border: 0; }
.cal-counts .cal-count.sev-info     { color: var(--sev-info);     background: none; border: 0; }
.cal-counts .cal-count.sev-normal   { color: var(--sev-normal);   background: none; border: 0; }
.cal-counts .cal-count.sev-unknown  { color: var(--sev-unknown);  background: none; border: 0; }

/* --- 3. the entry: a rule and a line, not a coloured block ----------------- */

.cal-chip {
  display: flex;
  align-items: baseline;
  gap: 5px;
  margin-top: 2px;
  padding: 1px 3px 1px 6px;
  border-left: 2px solid var(--sev-normal);
  background: none;
  border-radius: 0;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--ink-2);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
/* The ground is removed EXPLICITLY on each severity class, not just on the base
   .cal-chip. The v2 layer higher up this file sets a tinted background on
   `.cal-chip.sev-warning` and friends at the same specificity, so resetting the
   base alone left every warning entry amber and the rebuild half-applied - the
   exact failure this file's own note about hand-editing after the Tailwind
   build warns about. */
.cal-chip.sev-critical { border-left-color: var(--sev-critical); }
.cal-chip.sev-warning  { border-left-color: var(--sev-warning);  background: none; }
.cal-chip.sev-info     { border-left-color: var(--sev-info);     background: none; }
.cal-chip.sev-normal   { border-left-color: var(--sev-normal);   background: none; }
.cal-chip.sev-unknown  { border-left-color: var(--sev-unknown);  background: none; }

/* THE TWO THAT GET A FILL, and there are no others.
   A critical date is one that cannot be recovered from if it is missed. An
   overdue date is one that already has been. Everything else on this grid is
   work, and work does not need a colour. */
.cal-chip.sev-critical {
  background: var(--sev-critical-bg);
  border-left-width: 3px;
  color: var(--sev-critical);
  font-weight: 600;
}
.cal-chip.is-overdue {
  background: var(--sev-critical-bg);
  border-left-color: var(--sev-critical);
  border-left-width: 3px;
  color: var(--sev-critical);
  font-weight: 600;
}
.cal-chip .cal-glyph { width: 9px; font-size: 9.5px; flex: none; }
.cal-chip .cal-time { font-variant-numeric: tabular-nums; font-weight: 700; flex: none; color: var(--ink); }
.cal-chip.sev-critical .cal-time, .cal-chip.is-overdue .cal-time { color: inherit; }
.cal-chip .cal-what { overflow: hidden; text-overflow: ellipsis; }
/* Unchanged semantics, per the legend that is still printed on the surface. */
.cal-chip.is-fixed { border-left-style: double; border-left-width: 5px; }
.cal-chip.is-done  { opacity: .6; text-decoration: line-through; font-weight: 400; }
.cal-chip.is-undiarised { border-left-style: dashed; }
.cal-more { display: block; margin-top: 3px; font-size: 10.5px; font-weight: 700; color: var(--brand-2); }

/* --- 4. phone ------------------------------------------------------------- */

@media (max-width: 900px) {
  .cal-views { width: 100%; }
  .cal-views a { flex: 1 1 0; justify-content: center; padding: 6px 4px; }
  .cal-count { text-align: left; flex: 1 1 100%; }
}
@media (max-width: 760px) {
  .cal-head .cal-period { font-size: 17px; }
  .cal-filters .field.grow { flex: 1 1 100%; max-width: none; }
  .cal-month td { height: auto; min-height: 52px; }
  .cal-cellhead { flex-direction: column; align-items: center; gap: 1px; }
  .cal-daynum { line-height: 15px; }
  /* On a 44px-wide cell the counts ARE the content, so they always show. */
  .cal-counts, .cal-cell.is-truncated .cal-counts { display: flex; justify-content: center; gap: 4px; }
  .cal-chip, .cal-more, .cal-closed-name { display: none; }
}

@media print {
  /* The head is chrome. The count line is not: a printed month showing one
     person's dates and not saying so is the same defect on paper, and worse,
     because paper is what gets handed across a desk. */
  .cal-head-row, .cal-filters { display: none !important; }
  .cal-head { border-bottom: 0; }
  .cal-head-row2 { border-top: 0; padding-top: 0; }
  .cal-month td { height: auto; }
}

/* A tally is one word. "◆ 1" broke across two lines in a 44px phone cell,
   putting the glyph above its own number. */
.cal-counts .cal-count { white-space: nowrap; }
@media (max-width: 900px) {
  /* Renamed from .cal-count; the earlier rule was left pointing at the old
     name and the sentence stayed right-aligned on a phone, where it wrapped
     ragged-left against nothing. */
  .cal-shown { text-align: left; flex: 1 1 100%; }
}

/* ===========================================================================
   THE ASSISTANT: NEW CHAT, THREAD HISTORY, AND THE REVEAL. 31 Aug 2026.
   ======================================================================== */

.asst-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.asst-actions { display: flex; gap: 6px; flex: none; }
.asst-actions { align-items: center; }
/* The same 36px as the X beside them (Alex, 14 Sep 2026: "align the buttons height"). */
.asst-actions .btn.xsmall { padding: 0 12px; font-size: 12.5px; min-height: 36px; }
.asst-file-icon svg, .composer-attach svg { width: 18px; height: 18px; display: block; }

/* The thread picker is a panel INSIDE the dialog, never a second dialog: the
   house rule is one modal at a time, and a list of conversations stacked on
   top of the conversation it belongs to is the stack that rule prevents. */
.asst-threads {
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--bg);
  padding: 8px 10px;
  margin: 0 0 10px;
  max-height: 190px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.asst-threads-head {
  margin: 0 0 6px; font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.asst-thread-list { list-style: none; margin: 0; padding: 0; }
.asst-thread {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 7px 8px; border: 0; border-radius: var(--radius-s);
  background: none; font: inherit; color: var(--ink-2); font-size: 13px;
}
.asst-thread:hover { background: var(--card); color: var(--ink); }
.asst-thread:focus-visible { outline: 2px solid var(--brand-2); outline-offset: -2px; }
.asst-thread-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.asst-thread-w { flex: none; font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.asst-threads-empty { margin: 0; padding: 4px 2px; font-size: 12.5px; color: var(--muted); }

/* While an answer is being revealed the bubble is clickable, and says so. It
   is NOT a blinking caret: a caret asserts that something is still being
   written, and by this point the whole answer is already in the browser. */
.asst-msg.is-revealing { cursor: pointer; }

@media (max-width: 600px) {
  .asst-head { flex-direction: column; gap: 8px; }
  .asst-actions { width: 100%; }
  .asst-actions .btn.xsmall { flex: 1 1 0; justify-content: center; }
}
@media (pointer: coarse) {
  .asst-actions .btn.xsmall { min-height: 44px; }
  .asst-thread { min-height: 44px; align-items: center; }
}

/* ===========================================================================
   THE RECORD'S ALERTS: A BELL IN THE HEADING ROW. 31 Aug 2026.
   ===========================================================================
   Replaces the 300px alert column on the four record screens. Measured on
   TRP/2026/0007 at 1440x1000: the column squeezed the Deadlines table until
   "Discovery - list of documents to be filed" wrapped onto four lines and
   "Wong Kar Meng" onto three, while the alert beside it announced "1 deadline
   is overdue" and pointed at the row two inches to its left, already marked
   Overdue in red. On a record the column costs the most width and repeats the
   most, because a record already shows the thing being alerted about.

   The count and the worst severity stay on the screen at all times, so nothing
   about HOW MANY or HOW BAD is hidden behind the press; only the wording is.
   ======================================================================== */

.page-head.has-bell { align-items: center; }
.rec-bell-wrap { position: relative; flex: none; }

.rec-bell {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 34px; padding: 0 10px;
  border: 1px solid var(--line); border-radius: var(--radius-s);
  background: var(--card); color: var(--ink-2); cursor: pointer;
}
.rec-bell:hover { background: var(--line-soft); color: var(--ink); }
.rec-bell:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 1px; }
.rec-bell-n { font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* The bell takes the colour of the WORST thing behind it. That is the whole
   payment for putting the wording behind a press: severity never moves. */
.rec-bell.lvl-bad  { border-color: var(--bad);  background: var(--bad-bg);  color: var(--bad); }
.rec-bell.lvl-warn { border-color: var(--warn); background: var(--warn-bg); color: var(--warn); }

.rec-bell-panel {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  width: min(360px, calc(100vw - 32px));
  max-height: min(60vh, 460px); overflow-y: auto; overscroll-behavior: contain;
  padding: 10px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); box-shadow: 0 12px 32px rgba(20, 28, 40, .16);
  display: flex; flex-direction: column; gap: 8px;
}
.rec-bell-panel[hidden] { display: none; }
.rec-bell-head {
  margin: 0; font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.rec-bell-panel .alert { margin: 0; }

@media (max-width: 640px) {
  /* Anchored to the right edge of the screen rather than of a button that has
     wrapped somewhere unpredictable. */
  .rec-bell-panel { right: auto; left: 0; width: min(360px, calc(100vw - 24px)); }
}
@media (pointer: coarse) { .rec-bell { min-height: 44px; } }

@media print {
  /* On paper there is no press, so the alerts print open and in full. A
     printed record that silently omits "the CDD on this client is not
     finished" is worse than one that never had the panel. */
  .rec-bell { display: none !important; }
  .rec-bell-panel { position: static; display: flex !important; width: auto;
                    max-height: none; box-shadow: none; border: 0; padding: 0;
                    margin: 0 0 14px; }
  .rec-bell-panel[hidden] { display: flex !important; }
}

/* One-press follow-ups under the newest answer. Chips, not buttons: a button
   in this dialog acts, and every one of these only ever puts its own words in
   the question box. The label says "Ask next" for the same reason. */
.asst-chips { margin: 2px 0 4px; }
.asst-chips-lead {
  margin: 0 0 6px; font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.asst-chips-row { display: flex; flex-wrap: wrap; gap: 6px; }
.asst-chip {
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--card); color: var(--ink-2);
  padding: 6px 13px; font: inherit; font-size: 13px; line-height: 1.3;
  cursor: pointer; text-align: left;
  max-width: 100%; overflow-wrap: anywhere;
}
.asst-chip:hover { background: var(--line-soft); color: var(--ink); border-color: var(--brand-2); }
.asst-chip:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 1px; }
.asst-chip:active { transform: translateY(1px); }
@media (pointer: coarse) { .asst-chip { min-height: 44px; padding: 10px 15px; } }
@media (prefers-reduced-motion: reduce) { .asst-chip:active { transform: none; } }

/* ===========================================================================
   THE ALERT BELL MOVES INTO THE OVERLAY'S HEADER BAR. 31 Aug 2026.
   ===========================================================================
   Alex: the notification control belongs in the modal header, beside edit,
   print, PDF and email. It was in the record's own heading row, which is
   inside the frame and therefore a second bar of record-level controls below
   the one that already exists. One bar.

   The record's own bell is not deleted - it is the source the host reads the
   alerts from, it is the whole control when the record is opened outside the
   overlay, and it is what prints. Inside the overlay it is taken off the
   screen and nothing else.
   ======================================================================== */

.rec-bell-wrap.is-framed { display: none; }

/* First among the header tools: the one you read before you act. */
/* REDESIGNED 14 Sep 2026 (Alex: "this is ugly"). The bell was a tinted red
   box with a fat pill sitting on top of the glyph. It is now the same quiet
   control as the top bar's bell: a plain icon like the pencil and printer
   beside it, the count small on the bell's shoulder, the colour of the worst
   thing carried by the glyph and the count only. */
.rec-bell-tool { position: relative; gap: 0; }
.rec-bell-tool.lvl-bad,  .rec-bell-tool.lvl-bad:hover  { color: var(--bad);  border-color: var(--line); background: var(--card); }
.rec-bell-tool.lvl-warn, .rec-bell-tool.lvl-warn:hover { color: var(--warn); border-color: var(--line); background: var(--card); }
.rec-bell-tool.lvl-bad:hover, .rec-bell-tool.lvl-warn:hover { background: var(--line-soft); }
.rec-bell-tool .rec-bell-n {
  position: absolute; top: -5px; right: -5px;
  min-width: 17px; height: 17px; padding: 0 4px; border-radius: var(--r-xs);
  font-size: 10.5px; font-weight: 700; line-height: 17px;
  box-shadow: 0 0 0 2px var(--card, #fff);
}
.rec-bell-tool .rec-bell-n:empty { display: none; }

/* The panel drops out of the header bar, over the record, at the right. It is
   a disclosure and not a dialog: a second dialog on top of the record overlay
   is the stack the one-modal-at-a-time rule exists to prevent. */
.rec-alert-panel {
  position: absolute; top: 56px; right: 16px; z-index: 30;
  width: min(380px, calc(100% - 32px));
  max-height: min(60vh, 460px); overflow-y: auto; overscroll-behavior: contain;
  padding: 10px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); box-shadow: 0 14px 38px rgba(20, 28, 40, .18);
  display: flex; flex-direction: column; gap: 8px;
}
.rec-alert-panel[hidden] { display: none; }
.rec-alert-panel .alert { margin: 0; }
.alert-jump-btn {
  border: 0; background: none; padding: 0; font: inherit; font-size: 13px;
  font-weight: 600; color: var(--brand-2); cursor: pointer; text-decoration: underline;
}
.alert-jump-btn:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }

/* HOW WIDE A RECORD IS. THE ONE PLACE IT IS DECIDED.
   ==================================================

   The number is derived from what is inside the frame, not from the screen.
   Its history is three answers to the same question and only the middle one
   was ever measured:

     1180px  the record alone, two columns of content.
     1440px  + 300px for the alert column that used to sit inside the frame,
             + the 22px gap. 1180 + 322 = 1502, taken down to 1440.
     1600px  "nothing sits beside it any more, so use the screen."

   The third step kept the allowance for a column that had already been
   removed and then added to it. The alerts left the frame on 31 August 2026 -
   they are a bell in the overlay's own header bar now (page_aside_bell), and
   `.rec-bell-wrap.is-framed` takes the in-frame copy off the screen - so the
   300px is not merely unused, it is not there to use.

   Reported by Alex, 31 August 2026: the matter modal reads too wide. It is,
   and measurably rather than as a matter of taste. At 1600px the record's
   two columns are about 770px each, and the left one is a label/value list -
   CLIENT, STAGE, PARTNER, COURT - so every value sat a third of a metre from
   the word naming it and the eye had to travel the whole way for each line.
   A definition list is comfortable at 420-520px and the deadline table needs
   about 620px; with the 22px gap and the page's own 24px padding either side
   that is a little over 1200px of content and the rest was air inside a
   dialog, not margin around it.

   So: the pre-column measure, plus headroom for the wider tables further down
   the file (time, documents, bills), and capped so that on a large monitor
   this still reads as a record opened OVER the list it came from rather than
   a second application filling the screen. Below the cap it is the viewport
   less a real margin, which is the same behaviour as before. */
.ls-dialog.record { width: min(1240px, calc(100vw - 48px)); }

@media (max-width: 760px) {
  .ls-dialog.record { width: calc(100vw - 16px); }
  .rec-alert-panel { right: 8px; left: 8px; width: auto; top: 52px; }
}
@media print {
  .rec-bell-wrap.is-framed { display: block; }
  .rec-alert-panel { display: none !important; }
}

/* SEEN IS NOT APPROVED.

   Three states, three treatments, and the difference between the second and
   the third has to survive a glance:

     .tick-one   one tick, muted        answered, nobody has seen it
     .tick-seen  two ticks, muted       a lawyer's screen displayed it
     .tick-two   two ticks, brand, bold a lawyer reviewed it and stands behind it

   The seen pair is deliberately the SAME muted ink as the single tick, because
   what changed between those two states is only that somebody looked. Colour
   arrives with the decision, not with the sighting - which is also why the
   count of ticks alone is not the channel: a reader who sees no colour still
   has different words on the line. */
.tick-seen { color: var(--muted); font-weight: 600; }
.asst-state { font-size: 12px; color: var(--muted); margin: 6px 0 0; }

/* The bell with nothing behind it, and the bell that could not look.
   Both are drawn - a control that vanishes between one file and the next reads
   as a broken screen - and they must not look alike, because one of them means
   "checked, clear" and the other means "not checked". */
.rec-bell.lvl-info, .rec-bell-tool.lvl-info { color: var(--muted); }
.rec-bell.lvl-ok, .rec-bell-tool.lvl-ok { color: var(--muted); }
.rec-bell.lvl-unknown, .rec-bell-tool.lvl-unknown {
  color: var(--sev-unknown); border-color: var(--sev-unknown-line); background: var(--sev-unknown-bg);
}
.alert-ok { border-left-color: var(--ok, #1f7a4d); background: var(--ok-bg, #eef7f1); }
.alert-ok .alert-title { color: var(--ok, #1f7a4d); }

/* "This tab is a build behind." Stated once, never acted on by itself: a lawyer
   may have half a time entry in a form, and reloading to pick up a stylesheet
   would be a worse defect than the one it prevents. */
.ls-stale {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 14px; padding: 9px 12px;
  border: 1px solid var(--warn); border-left-width: 4px;
  border-radius: var(--radius-s);
  background: var(--warn-bg); color: var(--warn);
  font-size: 13px; line-height: 1.5;
}
.ls-stale .btn { min-height: 28px; padding: 3px 12px; font-size: 12.5px; }
.ls-stale-x {
  margin-left: auto; border: 0; background: none; cursor: pointer;
  font-size: 18px; line-height: 1; color: inherit; padding: 0 4px;
}
.ls-stale-x:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }
@media print { .ls-stale { display: none !important; } }

/* ---------------------------------------------------------------------------
   THE ACCOUNT MENU ON A CONSOLE, ON A PHONE, WAS AN UNNAMED ARROW.

   Two correct rules met and produced a wrong screen. Below 700px `.who-name`
   and the seat pill are hidden, because on a firm screen the top bar also
   carries the search box and there is no room for all three. On the Admin and
   Owner Consoles there IS no search box - search.php is tenant-scoped and the
   sidebar gates it on Db::hasFirm() - so the console's top bar was a 63px
   white band whose entire content was a bare 8px chevron, left-aligned, with
   no visible name of any kind.

   Measured 31 August 2026 at 375x812 on admin_firms.php: `<header class=
   "topbar">` held one child, `.who`, whose only visible descendant was
   `.usermenu-chev`. That control is the only way to reach Bug Reports,
   Feature Requests, Change Password, the other console and Sign Out on a
   phone, and nothing on the screen said what it was. The accessible name was
   there all along (`aria-label="Account menu"`), which is what kept this out
   of the keyboard and screen-reader passes - it is a defect only a sighted
   pointer user meets, which is most of them.

   So the name comes back exactly where the reason for hiding it does not
   apply: a top bar with no search box in it. Not the seat pill as well - the
   console rail already names the person and their seat in full at the top,
   and repeating that here would be the duplication the rail block refuses.
   One word beside the arrow makes the control a control.

   :has() is how "no search box in this bar" is asked, because that is the
   actual condition rather than a proxy for it, and it is behind @supports so
   an engine without it simply keeps today's behaviour rather than breaking.
   --------------------------------------------------------------------------- */
@supports selector(:has(*)) {
  @media (max-width: 700px) {
    .topbar:not(:has(.gsearch)) .who-name { display: inline; }
  }
}

/* ---------------------------------------------------------------------------
   "LAWSERVA IS WAITING ON YOU" - the notice that interrupts.

   Almost no new paint. This dialog is a decision surface with four things on
   it - what the report is, what was asked, a box to answer in, and the way to
   the full thread - and every one of them already has a treatment somewhere in
   this file. What it needs is the QUOTE set apart from the reader's own words,
   because those are the two blocks of prose on the screen and confusing them
   is confusing the question with the answer.

   The quote takes the brand rule down its left edge, which is the same mark
   `.tbl-subs tr.is-current` uses for "this is the one that matters", rather
   than a tinted box: a filled panel inside a dialog body reads as a second
   card and this is one card. `.fb-body` is reused verbatim for the text, so a
   question with line breaks reads here exactly as it reads in the thread.
   --------------------------------------------------------------------------- */
.fb-nudge-what { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 6px; }
.fb-nudge-title { margin: 0 0 14px; font-size: 16px; font-weight: 600; line-height: 1.35; }
.fb-nudge-ask {
  margin: 0 0 18px; padding: 2px 0 2px 14px;
  border-left: 3px solid var(--brand-2);
}
/* The attribution is a label, not a speaker in a transcript: small, quiet, and
   on its own line so the question below it starts at the left margin like any
   other paragraph. */
.fb-nudge-who {
  display: block; margin-bottom: 4px;
  font-size: 12px; font-weight: 600; letter-spacing: .02em;
  text-transform: uppercase; color: var(--muted);
}
/* Air above the count of the others, because it is a different subject from
   the box directly above it and read as a hint on that box without it. */
.fb-nudge-more { margin-top: 14px; }
/* The way out to the whole thread is a different subject from the sentence
   above it - the count of the others - and read as its continuation without
   this. */
.fb-nudge-link { margin-top: 14px; }

/* ---------------------------------------------------------------------------
   Coarse-pointer targets, 9 Sep 2026. A phone walk over every screen found
   two classes of control under the 44px floor the design protocol sets for
   a coarse pointer (CR-3): the checkbox and radio rows on settings and access
   forms (26px), and the record links inside table cells (18px tall text).
   Buttons and inputs already have their rule above. Labels grow to the floor
   and centre their box; a cell link gets vertical padding so the thumb has
   something to land on without changing what the row looks like on a desk.
   --------------------------------------------------------------------------- */
@media (pointer: coarse) {
  label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) {
    min-height: 44px; display: inline-flex; align-items: center; gap: 10px;
  }
  td a, .tbl-stack a { display: inline-block; padding: 10px 0; }
  td a.btn, .tbl-stack a.btn { padding: 10px 14px; }
}


/* ===========================================================================
   THE RIGHT-HAND ALERT SHEET. 11 Sep 2026.
   ===========================================================================
   Alex: "right side bar, right sheet, click then show up from the right, like
   macOS notifications", for every right-hand notification in the product.

   One surface replaces two: the 260px alert column beside the list screens
   (page_aside, which took a fifth of the width from the table it commented on)
   and the dropdown under the record bell. Both now open this sheet from a bell
   in the heading row - or, inside the record overlay, from the bell in the
   overlay's header bar. What stays on screen at all times is the COUNT and the
   colour of the WORST alert, on the bell; only the wording is behind the press.

   Visual language is countserva's (Alex, 11 Sep): slate neutrals, 16-18px
   radii, soft layered shadows in place of heavy borders, -50 tinted severity
   chips. Motion is macOS Notification Center's: a translucent material that
   slides in from the right edge and leaves the same way.

   The earlier `.rec-bell-panel` / `.rec-alert-panel` rules above are the
   dropdown this replaces; the `.ls-sheet` selectors below outrank them.
   ======================================================================== */

/* The bell. A pill with a word on it where there is room, because on a list
   screen it is standing in for a whole column the reader used to see. */
.rec-bell {
  gap: 6px; min-height: 36px; padding: 0 12px 0 10px;
  border-radius: 999px; font-size: 13px; font-weight: 600;
  box-shadow: 0 1px 2px rgb(16 24 40 / .05);
  transition: background-color .12s ease, border-color .12s ease, transform .1s ease;
}
.rec-bell:active { transform: scale(.97); }
.rec-bell-lbl { line-height: 1; }
.rec-bell .rec-bell-n {
  min-width: 19px; height: 19px; padding: 0 5px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums;
  background: var(--ink-2); color: #fff;
}
.rec-bell-tool .rec-bell-n { background: var(--ink-2); color: #fff; text-align: center; font-variant-numeric: tabular-nums; }
.rec-bell.lvl-bad .rec-bell-n,  .rec-bell-tool.lvl-bad .rec-bell-n  { background: var(--bad);  color: #fff; }
.rec-bell.lvl-warn .rec-bell-n, .rec-bell-tool.lvl-warn .rec-bell-n { background: var(--warn); color: #fff; }
.rec-bell.lvl-unknown .rec-bell-n, .rec-bell-tool.lvl-unknown .rec-bell-n { background: var(--sev-unknown); color: #fff; }
.rec-bell[aria-expanded="true"], .rec-bell-tool[aria-expanded="true"] {
  box-shadow: 0 0 0 3px rgb(27 84 136 / .16);
}

/* The sheet itself. */
.ls-sheet.rec-bell-panel,
.ls-sheet.rec-alert-panel {
  position: fixed; z-index: 70;
  top: var(--sheet-top, 10px); right: 10px; bottom: 10px; left: auto;
  width: min(400px, calc(100vw - 20px)); max-height: none;
  display: flex; flex-direction: column; gap: 0; padding: 0; overflow: hidden;
  color: var(--ink);
  background: rgb(244 246 249 / .86);
  -webkit-backdrop-filter: blur(26px) saturate(170%);
  backdrop-filter: blur(26px) saturate(170%);
  border: 1px solid rgb(255 255 255 / .8);
  border-radius: 18px;
  box-shadow:
    0 0 0 1px rgb(16 24 40 / .07),
    0 4px 14px -4px rgb(16 24 40 / .12),
    0 28px 60px -14px rgb(16 24 40 / .32);
  /* Off the right edge and back along the same path. The curve is the iOS
     sheet curve: fast out of the gate, long gentle settle, no overshoot. */
  transform: translate3d(calc(100% + 28px), 0, 0);
  opacity: .5;
  transition: transform .46s cubic-bezier(.32, .72, 0, 1), opacity .3s ease;
  will-change: transform;
  touch-action: pan-y;
  overscroll-behavior: contain;
}
.ls-sheet.is-open { transform: none; opacity: 1; }
.ls-sheet:not(.is-open) {
  pointer-events: none;
  transition: transform .32s cubic-bezier(.4, 0, .9, .6), opacity .26s ease;
}
.ls-sheet.is-dragging { transition: none; }
.ls-sheet[hidden] { display: none; }

/* Fixed header, fixed footer, only the body scrolls (the house modal rule,
   applied to the sheet so a reader who knows one knows the other): X at the
   top right, Close at the bottom left. */
.ls-sheet-head {
  flex: 0 0 auto; display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; padding: 16px 14px 12px 18px;
}
.ls-sheet-hgroup { min-width: 0; }
.ls-sheet-title {
  margin: 0; font-size: 17px; line-height: 1.25; font-weight: 700;
  letter-spacing: -.012em; color: var(--ink);
}
.ls-sheet-title:focus { outline: none; }
.ls-sheet-title:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 3px; border-radius: 4px; }
.ls-sheet-sub { margin: 3px 0 0; font-size: 12.5px; line-height: 1.4; color: var(--muted); }
.ls-sheet-asof { white-space: nowrap; }
.ls-sheet-x {
  flex: none; width: 32px; height: 32px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 999px; cursor: pointer;
  background: rgb(16 24 40 / .06); color: var(--ink-2);
  transition: background-color .12s ease, transform .1s ease;
}
.ls-sheet-x:hover { background: rgb(16 24 40 / .11); color: var(--ink); }
.ls-sheet-x:active { transform: scale(.94); }
.ls-sheet-x:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }
.ls-sheet-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  overscroll-behavior: contain; scrollbar-width: thin;
  display: flex; flex-direction: column; gap: 8px;
  padding: 2px 12px 14px;
}
.ls-sheet-foot {
  flex: 0 0 auto; display: flex; justify-content: flex-start; gap: 8px;
  padding: 10px 14px 12px; border-top: 1px solid rgb(16 24 40 / .07);
}

/* A card per alert, macOS-notification shaped: glyph chip, severity word,
   title, body, one action. Colour + glyph SHAPE + word - three channels. */
.ls-sheet .ls-note {
  margin: 0; display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 11px;
  align-items: start; padding: 12px 14px 12px 12px;
  border: 0; border-radius: 14px; background: rgb(255 255 255 / .94);
  box-shadow: 0 1px 2px rgb(16 24 40 / .05), 0 4px 12px -3px rgb(16 24 40 / .08);
  font-size: 13.5px;
}
.ls-sheet .ls-note-bad  { box-shadow: inset 3px 0 0 var(--bad),  0 1px 2px rgb(16 24 40 / .05), 0 4px 12px -3px rgb(16 24 40 / .08); }
.ls-sheet .ls-note-warn { box-shadow: inset 3px 0 0 var(--warn), 0 1px 2px rgb(16 24 40 / .05), 0 4px 12px -3px rgb(16 24 40 / .08); }
.ls-note-ico {
  width: 30px; height: 30px; border-radius: 9px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--line-soft); color: var(--ink-2);
}
.ls-note-bad .ls-note-ico     { background: var(--bad-bg);  color: var(--bad); }
.ls-note-warn .ls-note-ico    { background: var(--warn-bg); color: var(--warn); }
.ls-note-ok .ls-note-ico      { background: var(--ok-bg);   color: var(--ok); }
.ls-note-unknown .ls-note-ico { background: var(--sev-unknown-bg); color: var(--sev-unknown); }
.ls-note-main { min-width: 0; }
.ls-note-sev {
  margin: 0 0 2px; font-size: 11px; font-weight: 700; line-height: 1.3;
  letter-spacing: .05em; text-transform: uppercase; color: var(--muted);
}
.ls-note-bad .ls-note-sev     { color: var(--bad); }
.ls-note-warn .ls-note-sev    { color: var(--warn); }
.ls-note-ok .ls-note-sev      { color: var(--ok); }
.ls-note-unknown .ls-note-sev { color: var(--sev-unknown); }
.ls-sheet .ls-note .alert-title {
  margin: 0; font-size: 14px; font-weight: 650; line-height: 1.35; color: var(--ink);
  overflow-wrap: anywhere;
}
.ls-sheet .ls-note .alert-body {
  margin: 3px 0 0; font-size: 13px; line-height: 1.45; color: var(--ink-2);
  overflow-wrap: anywhere;
}
.ls-sheet .ls-note .alert-act { margin: 9px 0 0; }
.ls-sheet .alert-act a,
.ls-sheet .alert-jump-btn {
  display: inline-flex; align-items: center; gap: 4px;
  min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px; cursor: pointer;
  background: rgb(27 84 136 / .09); color: var(--brand-2);
  font: inherit; font-size: 12.5px; font-weight: 600; text-decoration: none;
  transition: background-color .12s ease, transform .1s ease;
}
.ls-sheet .alert-act a:hover,
.ls-sheet .alert-jump-btn:hover { background: rgb(27 84 136 / .16); }
.ls-sheet .alert-act a:active,
.ls-sheet .alert-jump-btn:active { transform: scale(.97); }
.ls-sheet .alert-act a:focus-visible,
.ls-sheet .alert-jump-btn:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }

/* The cards arrive a beat behind the sheet, nearest first - the "grow toward
   you" cue, kept short so it never delays reading. */
@keyframes ls-note-in {
  from { opacity: 0; transform: translate3d(18px, 0, 0); }
  to   { opacity: 1; transform: none; }
}
.ls-sheet.is-open .ls-note { animation: ls-note-in .42s cubic-bezier(.32, .72, 0, 1) both; }
.ls-sheet.is-open .ls-note:nth-child(2) { animation-delay: .03s; }
.ls-sheet.is-open .ls-note:nth-child(3) { animation-delay: .06s; }
.ls-sheet.is-open .ls-note:nth-child(4) { animation-delay: .09s; }
.ls-sheet.is-open .ls-note:nth-child(n+5) { animation-delay: .12s; }

/* Inside the record overlay: the sheet belongs to the record, so it slides
   out from under the dialog's own right edge, between its header bar and its
   footer (heights measured by LS.sheet at open). The dialog clips it. */
.ls-dialog.record { overflow: hidden; }
.ls-dialog.record .ls-sheet.rec-alert-panel {
  position: absolute;
  top: var(--sheet-top, 68px); bottom: var(--sheet-bottom, 72px); right: 10px;
  width: min(400px, calc(100% - 20px));
}

@media (max-width: 640px) {
  .ls-sheet.rec-bell-panel,
  .ls-sheet.rec-alert-panel { top: 8px; right: 8px; bottom: 8px; left: 8px; width: auto; border-radius: 16px; }
  .ls-dialog.record .ls-sheet.rec-alert-panel { top: var(--sheet-top, 60px); bottom: var(--sheet-bottom, 64px); }
  .rec-bell-lbl { display: none; }
}
@media (pointer: coarse) {
  .ls-sheet-x { width: 44px; height: 44px; }
  .ls-sheet .alert-act a, .ls-sheet .alert-jump-btn { min-height: 44px; padding: 0 16px; }
  .rec-bell { min-height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
  .ls-sheet.rec-bell-panel, .ls-sheet.rec-alert-panel,
  .ls-sheet:not(.is-open) { transform: none !important; transition: opacity .2s ease; }
  .ls-sheet.is-open .ls-note { animation: none; }
  .rec-bell:active, .ls-sheet-x:active { transform: none; }
}
@media (prefers-reduced-transparency: reduce) {
  .ls-sheet.rec-bell-panel, .ls-sheet.rec-alert-panel {
    background: #f4f6f9; -webkit-backdrop-filter: none; backdrop-filter: none;
  }
}
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .ls-sheet.rec-bell-panel, .ls-sheet.rec-alert-panel { background: #f4f6f9; }
}
@media (prefers-contrast: more) {
  .ls-sheet.rec-bell-panel, .ls-sheet.rec-alert-panel {
    background: #fff; -webkit-backdrop-filter: none; backdrop-filter: none; border-color: var(--line);
  }
  .ls-sheet .ls-note { box-shadow: none; border: 1px solid var(--line); }
}
@media print {
  /* On paper there is no press, so the alerts print open and in full. */
  .ls-sheet.rec-bell-panel {
    position: static !important; display: flex !important; transform: none !important;
    opacity: 1 !important; width: auto !important; box-shadow: none !important;
    background: none !important; border: 0 !important; border-radius: 0 !important;
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important; margin: 0 0 14px;
  }
  .ls-sheet .ls-sheet-x, .ls-sheet .ls-sheet-foot { display: none !important; }
  .ls-sheet .ls-sheet-body { overflow: visible; padding: 0; }
  .ls-sheet .ls-note { box-shadow: none !important; border: 1px solid #ccc; break-inside: avoid; }
}


/* ===========================================================================
   SAAS REFRESH, STAGE 1: TOKENS, TYPE AND SHARED SURFACES. 11 Sep 2026.
   ===========================================================================
   Alex: "a modern style SaaS lawyer management and law firm operation
   management system ... refer to countserva style and theme". Stage 1 of four
   (1 tokens/type/surfaces, 2 app shell, 3 tables/forms/cards/dialogs,
   4 screen-by-screen pass). Countserva's language, adapted: slate neutrals,
   16px card radii, soft layered shadows instead of hard borders, a tinted
   table head, soft focus rings, a solid pill for the current nav item.

   DELIBERATELY NOT TAKEN FROM COUNTSERVA:
   - its pale input border (#d6dae1). Fields keep --field-line, set for WCAG
     1.4.11 non-text contrast; a field you cannot see the edge of is a defect.
   - Google Fonts. The app's CSP is font-src 'self'. Inter is first in the
     stack and used where installed; everyone else gets the platform UI face
     (San Francisco on a Mac, Segoe UI on Windows), which is its equal.
   - its brand blue. LawServa keeps its navy; severity colours are frozen
     (design protocol 8) and untouched here.

   This is one layer at the END of the file on purpose: every rule in it wins
   by source order, and removing this block restores the previous look exactly.
   ======================================================================== */
:root {
  --bg: #f4f6f9;
  --card: #ffffff;
  --ink: #101828;
  --ink-2: #344054;
  --muted: #5b6474;
  --line: #e4e8ee;
  --line-soft: #eef1f5;
  --color-line: #e4e8ee;
  --radius: 16px;
  --radius-s: 8px;
  --shadow: 0 1px 2px rgb(16 24 40 / .04), 0 4px 12px -2px rgb(16 24 40 / .06);
  /* A solid 1px brand edge inside the soft halo: with the 1px brand border
     that is a 2px dark edge, so focus never rests on the halo alone. */
  --ring: 0 0 0 1px var(--brand-2), 0 0 0 4px rgb(27 84 136 / .18);
  --font-ui: "Inter", "InterVariable", -apple-system, BlinkMacSystemFont, "Segoe UI",
             Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif;
}
/* The high-contrast preference still wins over the lighter lines above. */
@media (prefers-contrast: more) {
  :root { --line: #9aa7b5; --color-line: #9aa7b5; --muted: #4d5966; }
}

body {
  font-family: var(--font-ui);
  letter-spacing: -.006em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
td, th, input, .money, .num { font-variant-numeric: tabular-nums; }
.page-head h1 { font-size: 22px; font-weight: 650; letter-spacing: -.02em; line-height: 1.25; }
h2 { letter-spacing: -.012em; }

/* Surfaces: a card is a raised plane, made by tone and a soft shadow, not a
   hard outline. */
.card {
  border-color: var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.topbar { border-bottom-color: var(--line); }

/* Tables: a tinted head in small caps, lighter row rules, a quiet hover. */
thead th {
  background: #f8fafc;
  color: #475569;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .04em;
}
thead th:first-child { border-top-left-radius: 10px; }
thead th:last-child  { border-top-right-radius: 10px; }
th, td { border-bottom-color: #edf0f4; }
tbody tr:hover { background: #f8fafc; }

/* Buttons: rounder, a hint of depth on the primary, a press you can feel. */
.btn { border-radius: 10px; }
.btn:not(.secondary):not(.danger):not(.ghost):not(.link) {
  box-shadow: 0 1px 2px rgb(18 57 95 / .22);
}
.btn.secondary { border-color: #d0d5dd; box-shadow: 0 1px 2px rgb(16 24 40 / .05); }
.btn.secondary:hover { background: #f8fafc; border-color: #b9c1cc; }
.btn:active { transform: scale(.98); }

/* Fields: same edge colour as before (contrast), softer corners, and the
   focus state becomes a brand border plus a soft ring. */
input[type=text], input[type=email], input[type=password], input[type=date],
input[type=time], input[type=tel], input[type=url], input[type=number],
input[type=search], select, textarea {
  border-radius: 10px;
  transition: border-color .12s ease, box-shadow .12s ease;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brand-2);
  box-shadow: var(--ring);
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--brand-2);
  box-shadow: var(--ring);
}

/* Sidebar: the current screen is a solid pill, not a tint with an inset bar. */
.side .sidenav a { border-radius: 9px; }
.side .sidenav a:hover { background: rgb(255 255 255 / .07); }
.side .sidenav a[aria-current="page"] {
  background: #1f4f80;
  color: #fff;
  font-weight: 600;
  box-shadow: 0 1px 2px rgb(0 0 0 / .25);
}
.brand-mark { border-radius: 10px; }

/* Dialogs share the card radius. */
.ls-dialog { border-color: var(--line); border-radius: var(--radius);
  box-shadow: 0 24px 60px -12px rgb(16 24 40 / .32), 0 2px 8px rgb(16 24 40 / .08); }

/* New content arrives with a short fade. Opacity only: a transform here would
   make .page the containing block for the fixed alert sheet mid-animation. */
@keyframes ls-page-in { from { opacity: 0; } to { opacity: 1; } }
.main > .page { animation: ls-page-in .16s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  .main > .page { animation: none; }
  .btn:active { transform: none; }
}
@media print {
  .card { box-shadow: none; }
  thead th { background: none; }
  .main > .page { animation: none; }
}


/* ===========================================================================
   SAAS REFRESH, STAGES 2 AND 3: THE SHELL, AND THE PARTS. 12 Sep 2026.
   ===========================================================================
   Follows stage 1 above and works the same way: one labelled layer, removable
   whole. Stage 2 is the app shell (type face, sidebar, top bar, page heads,
   tab strips); stage 3 is the parts every screen is built from (pills, stat
   tiles, dialogs, empty states, record links).
   ======================================================================== */

/* Inter, self-hosted: the CSP is font-src 'self', so it lives in assets/fonts
   (Fontsource latin builds, SIL OFL 1.1 - see assets/fonts/README.txt).
   swap: text is readable in the fallback face while the file arrives. */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/inter-400.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("fonts/inter-500.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("fonts/inter-600.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("fonts/inter-700.woff2") format("woff2"); }
body { font-feature-settings: "cv11", "ss01"; }

/* ---- Stage 2: the shell ------------------------------------------------ */

/* Sidebar group labels: quieter, so the links are what the eye finds. */
.navgroup { color: rgb(255 255 255 / .46); letter-spacing: .08em; font-weight: 600; font-size: 10.5px; }
.side .sidenav a { font-weight: 500; }
.firmname { color: rgb(255 255 255 / .6); }

/* The top bar search reads as a field sitting in the bar: a light fill,
   the same accessible edge as every other field. */
.gsearch input { background: #f8fafc; border-radius: 10px; }
.gsearch input:focus { background: #fff; outline: none; border-color: var(--brand-2); box-shadow: var(--ring); }

/* Page heads: the title and its one-line summary as a pair. */
.page-head { margin-bottom: 20px; }
.page-head .sub { font-size: 13.5px; margin-top: 4px; }

/* Section tabs: a clean underline. The .subnav filter strip (Open / On hold /
   Closed / All) is NOT touched - it is a segmented control on purpose, "a
   filter, not a location" (layout.php page_filters), and styling it as tabs
   would say the opposite. */
.tabs { gap: 4px 20px; border-bottom-color: var(--line); }
.tabs a {
  padding: 10px 2px; color: #5b6474; font-weight: 500;
  border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: none;
  margin-bottom: -1px;
}
.tabs a:hover { color: var(--ink); }
.tabs a[aria-current=page] {
  color: var(--brand-2); border-bottom-color: var(--brand-2); background: none; font-weight: 600;
}

/* ---- Stage 3: the parts ------------------------------------------------ */

/* Status pills: tinted, with a hairline in the same hue so they hold their
   shape on white and on the tinted table head. Colours are the frozen
   severity tokens - only the shape changes. */
.pill { font-size: 11.5px; font-weight: 600; padding: 2px 9px; letter-spacing: .01em;
  box-shadow: inset 0 0 0 1px var(--line); }
.pill.bad  { box-shadow: inset 0 0 0 1px var(--bad-line); }
.pill.ok   { box-shadow: inset 0 0 0 1px var(--ok-line); }
.pill.warn { box-shadow: inset 0 0 0 1px var(--warn-line); }

/* Stat tiles: the number carries the tile. */
.stat .n { font-weight: 650; letter-spacing: -.025em; }

/* Dialogs: a heading that matches the page heads, a footer that reads as
   the dialog's base rather than a strip of page. */
.ls-dialog h2 { font-size: 17px; font-weight: 650; letter-spacing: -.015em; }
.dialog-actions { background: #fcfcfd; border-top-color: var(--line); padding: 14px 24px; }

/* Empty states: calm, centred, muted - never louder than content. */
.empty { color: var(--muted); }

/* Record links in tables: the thing you click, weighted like it. */
td a:not(.btn) { font-weight: 600; color: var(--brand-2); text-decoration: none; }
td a:not(.btn):hover { text-decoration: underline; text-underline-offset: 2px; }

/* ---------------------------------------------------------------------------
   OWNERS, ONE FLAT ROW EACH. 12 Sep 2026 - adapted from a client register
   Alex supplied: dense rows, a coloured "N DAYS" badge per law firm, a Status
   badge, and plain glyph actions instead of boxed buttons. The band, card and
   per-firm table layouts tried before it were removed from this file on
   12 Sep 2026 once nothing rendered them.
   --------------------------------------------------------------------------- */
.tbl-ow td, .tbl-ow th { padding: 12px 14px; vertical-align: top; }
.tbl-ow thead th { white-space: nowrap; }
.tbl-ow tbody > tr > td { border-bottom: 1px solid var(--line); }
.tbl-ow tbody > tr:last-child > td { border-bottom: 0; }
.tbl-ow tbody > tr:nth-child(even) > td { background: var(--surface-sunk); }
@media (hover: hover) and (pointer: fine) {
  .tbl-ow tbody > tr:hover > td { background: var(--s-hover); }
}
.tbl-ow tbody > tr.is-unowned > td:first-child { box-shadow: inset 3px 0 0 var(--warn); }

.tbl-ow .ow-name { font-weight: 600; color: var(--ink); font-size: 14px; text-align: left; }
.tbl-ow button.ow-name:hover, .tbl-ow button.ow-name:focus-visible { color: var(--brand-2); }
.tbl-ow .ow-sub { margin-top: 2px; font-size: 12.5px; color: var(--muted); }
.tbl-ow .ow-sub .linkbtn { font-size: 12.5px; color: var(--muted); font-weight: 400; }
.tbl-ow .ow-sub .linkbtn:hover { color: var(--brand-2); }
.tbl-ow .ow-ico { font-size: 12px; }
.tbl-ow .ow-email { color: var(--ink-2); overflow-wrap: anywhere; }

/* A LINE per person or per firm inside one cell, its glyphs at its right. */
.tbl-ow .ow-line { display: flex; align-items: flex-start; gap: 10px; }
.tbl-ow .ow-line + .ow-line { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); }
.tbl-ow .ow-line-main { flex: 1 1 auto; min-width: 0; }
.tbl-ow .ow-line.is-inside { background: var(--brand-tint); border-radius: var(--r-xs); padding: 4px 6px; }
.tbl-ow .ow-firm { font-weight: 500; color: var(--brand-2); text-align: left; margin-left: 6px; }
.tbl-ow .ow-tools { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px; }
.tbl-ow .ow-tools form { display: inline-flex; margin: 0; }

/* THE BADGE. Small caps, a tinted ground and a solid word: the value is the
   text, the colour only its ramp (never the only channel). Tokens only. */
.tbl-ow .badge {
  display: inline-flex; align-items: center; height: 20px; padding: 0 7px;
  border-radius: var(--r-xs); border: 1px solid transparent;
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; line-height: 1; white-space: nowrap;
  vertical-align: 1px; font-family: inherit;
}
.tbl-ow .badge.lg { height: 24px; padding: 0 10px; font-size: 11px; }
.tbl-ow .badge.ok   { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-line); }
.tbl-ow .badge.warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.tbl-ow .badge.bad  { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }
.tbl-ow .badge.info { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }
.tbl-ow .badge.mute { background: var(--surface-active); color: var(--muted); border-color: var(--line); }
.tbl-ow button.badge { cursor: pointer; }
.tbl-ow button.badge:hover, .tbl-ow button.badge:focus-visible { border-color: currentColor; }

/* THE GLYPH ACTION. No box, no fill: muted until the pointer or focus is on
   it. 30px on a mouse, 44px on a coarse pointer (Gate D). */
.tbl-ow .gi {
  display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r-sm); background: transparent;
  color: var(--muted); cursor: pointer; text-decoration: none;
}
.tbl-ow .gi svg { width: 17px; height: 17px; }
.tbl-ow .gi:hover, .tbl-ow .gi:focus-visible { background: var(--surface-active); color: var(--ink); border-color: var(--line); }
.tbl-ow .gi:focus-visible { outline: none; box-shadow: var(--ring); }
.tbl-ow .gi.danger { color: var(--bad); }
.tbl-ow .gi.danger:hover, .tbl-ow .gi.danger:focus-visible { background: var(--bad-bg); border-color: var(--bad-line); }
.tbl-ow .gi.strong { color: var(--brand-2); }
@media (pointer: coarse) { .tbl-ow .gi { width: 44px; height: 44px; } }

@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow { table-layout: fixed; width: 100%; min-width: 900px; }
  .tbl-ow .c-no    { width: 48px; }
  .tbl-ow .c-owner { width: 20%; }
  .tbl-ow .c-login { width: 26%; }
  .tbl-ow .c-state { width: 104px; }
  .tbl-ow .c-act   { width: 172px; }
}
@media (max-width: 760px) {
  .tbl-ow.is-stacked td.c-no { display: none; }
  .tbl-ow.is-stacked td.c-owner {
    display: block; padding: 0 0 10px; margin-bottom: 6px; border-bottom: 1px solid var(--line-soft);
  }
  .tbl-ow.is-stacked td.c-owner::before { content: none; }
  .tbl-ow.is-stacked tbody > tr > td { border-bottom: 0; background: transparent; }
  .tbl-ow.is-stacked tr.is-unowned > td:first-child { box-shadow: none; }
}

/* The law firm line is the point of this table, so it gets the width: badge
   and name on one line, the period on one line under it, as in the reference.
   The owner's five glyphs need 186px; the column was 172 and clipped the last. */
@media (min-width: 761px) {
  .tbl-ow .c-owner { width: 17%; }
  .tbl-ow .c-login { width: 22%; }
  .tbl-ow .c-act   { width: 200px; }
  .tbl-ow .ow-period { white-space: nowrap; }
}
.tbl-ow .c-act .ow-tools { flex-wrap: nowrap; }

/* AN ADDRESS NEVER BREAKS MID-WORD. "rival@lawserva.co" over "m" reads as a
   typo. One line, ellipsis if it must, the whole address in the title and in
   the person's details. */
.tbl-ow .ow-email {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal;
}

/* ON A PHONE the two cells that hold LINES (people, firms) take the card's full
   width with their label above, instead of the stacked label-and-value grid,
   which left them a 90px strip beside their glyphs and broke names letter by
   letter. Each line's glyphs drop under its text. */
@media (max-width: 760px) {
  .tbl-ow.is-stacked td.c-login,
  .tbl-ow.is-stacked td.c-firms { display: block; }
  .tbl-ow.is-stacked td.c-login::before,
  .tbl-ow.is-stacked td.c-firms::before { display: block; margin: 8px 0 6px; }
  .tbl-ow.is-stacked .ow-line { flex-wrap: wrap; }
  .tbl-ow.is-stacked .ow-line-main { flex: 1 1 100%; }
  .tbl-ow.is-stacked .ow-tools { margin-left: -6px; }
  .tbl-ow.is-stacked .ow-email { white-space: normal; overflow-wrap: anywhere; }
}

/* ---------------------------------------------------------------------------
   OWNER DIALOG AND LINK NAMES. 12 Sep 2026.

   NAMES ARE ALWAYS LINKS. Alex: "do not hover owner name only you change text
   color and underline, please always like a link, like the others". The owner
   and firm names on the Owners table open a dialog, so they wear the link
   colour at rest, the way every other record name in LawServa does - a name
   that only turns into a link under the pointer is a control nobody finds.

   TABS ARE RADIOS. Two tabs (General, Law Firms), no script: the checked radio
   decides which panel and which footer act show, through :has(). The radios
   are visually hidden but stay in the tab order and keep a focus ring on
   their label.
   --------------------------------------------------------------------------- */
.tbl-ow .ow-name,
.tbl-ow .ow-firm { color: var(--brand-2); text-decoration: none; }
.tbl-ow .ow-name { font-weight: 600; }
.tbl-ow .ow-name:hover, .tbl-ow .ow-name:focus-visible,
.tbl-ow .ow-firm:hover, .tbl-ow .ow-firm:focus-visible { color: var(--brand); text-decoration: underline; }

/* DIALOG TABS, ONE COMPONENT FOR EVERY MODAL (Alex, 14 Sep 2026: "design a
   better ui ux for this part, for all modals"). A segmented control, not a
   row of underlined words: the tabs sit in one soft track so the reader sees
   the whole set as ONE choice, and the chosen one is a raised white segment,
   which is the same mark the sidebar and the page filters use for "you are
   here". Counts are quiet chips inside the segment. `.dlg-tabs` is the shared
   name; `.own-tabs` (the owner dialog) is the first user and keeps working.
   Motion: background and shadow only, 110ms, and none under reduced motion. */
.own-tabs, .dlg-tabs {
  flex: none; display: flex; gap: 3px; padding: 3px; margin: 14px 24px 0;
  background: var(--bg); border: 1px solid var(--line); border-radius: 12px;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
}
.own-tabs::-webkit-scrollbar, .dlg-tabs::-webkit-scrollbar { display: none; }
.own-tab-r { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.own-tabs label, .dlg-tabs label {
  flex: 1 0 auto; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 34px; padding: 6px 14px; border-radius: 9px; border: 0; margin: 0;
  font-size: 13.5px; font-weight: 500; color: var(--muted); cursor: pointer; white-space: nowrap;
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}
.own-tabs label:hover, .dlg-tabs label:hover { color: var(--ink); background: rgb(255 255 255 / .6); }
.own-tab-r:checked + label {
  color: var(--brand); background: var(--card); font-weight: 600;
  box-shadow: var(--e-1), 0 0 0 1px var(--line);
}
.own-tab-r:focus-visible + label { box-shadow: var(--ring); }
.own-tab-n {
  display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px;
  padding: 0 6px; border-radius: var(--r-full); background: var(--surface-active);
  font-size: 11px; font-weight: 600; line-height: 1; color: var(--ink-2);
}
.own-tab-r:checked + label .own-tab-n { background: var(--brand-tint); color: var(--brand); }
@media (prefers-reduced-motion: reduce) { .own-tabs label, .dlg-tabs label { transition: none; } }


.own-panel { padding-top: 14px; }
.own-sec-h { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 4px 0 10px; }
.own-sec-h h3 { margin: 0; font-size: 14px; font-weight: 600; color: var(--ink); }
.own-sec-h a { font-size: 13px; color: var(--brand-2); }
.own-panel .form-grid + .own-sec-h { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); }

/* OWNER DIALOG, THREE TABS (Profile, Login, Law Firms). Subscriptions merged
   into Law Firms on 14 Sep 2026. Save Changes serves every tab (one form; the
   owner account term lives on Law Firms); New Law Firm serves Law Firms. */
.own-dlg .own-panel,
.own-dlg .own-foot-save,
.own-dlg .own-foot-firm { display: none; }
.own-dlg:has(.own-t-profile:checked) .p-profile,
.own-dlg:has(.own-t-login:checked) .p-login,
.own-dlg:has(.own-t-firms:checked) .p-firms { display: block; }
.own-dlg:has(.own-t-profile:checked) .own-foot-save,
.own-dlg:has(.own-t-login:checked) .own-foot-save,
.own-dlg:has(.own-t-firms:checked) .own-foot-save,
.own-dlg:has(.own-t-firms:checked) .own-foot-firm { display: inline-flex; }
/* Law Firms tab, countserva's Manage Owner shape (14 Sep 2026): tiles, then
   sectioned tables with a one-line description under each heading. */
.own-metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 0 0 18px; }
.own-metrics .metric { padding: 12px 14px 13px; }
.own-metrics .metric-n { font-size: 22px; }
.own-metrics .own-metric-date { font-size: 17px; letter-spacing: -.01em; }
.own-sec-sub { margin: -6px 0 12px; }
.own-term-grid { margin-bottom: 20px; }
.own-tblwrap { border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; margin: 0 0 22px; }
.own-tblwrap .own-ftbl thead th { background: var(--bg); }
.own-tblwrap .own-ftbl tbody tr:hover > td { background: var(--bg); }
.own-tblwrap .own-ftbl tr.is-inside > td { background: var(--brand-tint); }
@media (max-width: 760px) { .own-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.own-dlg .own-link { color: var(--brand-2); font-weight: 500; text-align: left; }
.own-dlg .own-link:hover, .own-dlg .own-link:focus-visible { color: var(--brand); text-decoration: underline; }
.own-dlg .own-ftbl tr.is-current > td { background: var(--brand-tint); }
.own-panel > .own-sec-h:first-child { margin-top: 0; }

/* One badge shape everywhere in the owner dialog. It was scoped to two
   containers and the Subscriptions tab's firm headers were in neither, so
   their "364 DAYS" rendered as bare coloured text. */
.own-dlg .badge {
  display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: var(--r-xs);
  border: 1px solid transparent; font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
  line-height: 1; white-space: nowrap;
}
.own-panel .hint + .own-sec-h,
.own-panel .form-grid + .hint + .own-sec-h { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); }

/* FOUR TABS FIT A PHONE. At 375px they measured 392px in a 337px strip, so
   "Subscriptions" sat behind a scrollbar. Tighter padding below 760px fits all
   four; the strip still scrolls sideways (never vertically) as a fallback,
   with the bar hidden because it read as a stray grey rule under the tabs. */
@media (max-width: 760px) {
  .own-tabs, .dlg-tabs { margin-left: 16px; margin-right: 16px; }
  .own-tabs label, .dlg-tabs label { padding: 6px 10px; font-size: 13px; gap: 5px; }
  .own-tab-n { min-width: 18px; padding: 0 5px; }
}

/* ===========================================================================
   ASSISTANT: "FOR YOU TODAY". 12 Sep 2026.

   The list the panel opens with, above the log: measured facts from this
   firm's records about the person reading, never model output, and styled
   like the thread picker (a panel inside the dialog on the sunk ground), not
   like an answer bubble. Severity is carried three ways - the reserved ramp
   tokens, a glyph, and a spoken prefix in the text - so it survives a
   monochrome print and a screen reader (design protocol 4.A, CR-4). A row is
   one sentence, then "Ask" (a chip: it puts the question in the box and
   sends it) and "Open" (a link to the screen that does the job). Folded, the
   section is its one-line summary and nothing else.
   =========================================================================== */
.asst-today {
  /* Shrinks before the log does and never below its own head: measured on a
     375x812 phone, a non-shrinking panel pushed the dialog's footer 67px past
     the bottom of the screen and the head's subtitle under the panel. */
  flex: 0 1 auto;
  min-height: 34px;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--bg);
  padding: 6px 10px 8px;
  margin: 0 0 10px;
  max-height: 34vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.asst-today.folded { padding-bottom: 6px; max-height: none; overflow: visible; }
/* The head never shrinks. It was flex: 0 1 auto, so any panel below it -
   this one, the thread picker - pushed the subtitle under the panel on a
   phone. The log and this panel are the parts that give, and both scroll. */
/* Point-form answers (14 Sep 2026): real lists inside the bubble. */
.asst-body { margin: 0; }
.asst-body p { margin: 0 0 8px; }
.asst-body p:last-child { margin-bottom: 0; }
.asst-body ol, .asst-body ul { margin: 2px 0 10px 22px; padding: 0; display: grid; gap: 5px; }
.asst-body ol { list-style: decimal; }
.asst-body ul { list-style: disc; }
.asst-body li { padding-left: 2px; }
.asst-body li::marker { font-weight: 600; color: var(--brand); }
.asst-body .asst-next { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line-soft, var(--line)); }
.asst-body .asst-next strong { color: var(--brand); }
.ls-dialog.assistant > .asst-head { flex-shrink: 0; }
/* ONE ROW, ONE BASELINE (Alex, 14 Sep 2026: "why some up some down?"). The
   X is absolute at top:12px; the head's own padding-top is 20px, so New Chat
   and History sat 8px lower than the X beside them. The X now sits on the
   head's padding line, level with the two buttons, with the same 6px gap
   the buttons keep between themselves. */
.ls-dialog.assistant > .dialog-x { top: 20px; right: 22px; }
.ls-dialog.assistant > .asst-head { padding-right: 64px; }
.asst-today-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.asst-today-toggle {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  background: none; border: 0; padding: 4px 2px; margin: 0 0 0 -2px;
  font: inherit; color: var(--ink); cursor: pointer; border-radius: var(--radius-s);
}
.asst-today-toggle:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 1px; }
.asst-today-chev {
  width: 7px; height: 7px; flex: none;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg); transition: transform var(--t-fast, 120ms) var(--ease-out, ease);
  margin-top: -3px;
}
.asst-today-toggle[aria-expanded="false"] .asst-today-chev { transform: rotate(-45deg); margin-top: 0; }
.asst-today-title { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.asst-today-sum { font-size: 12.5px; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.asst-today-asof { flex: none; font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

.asst-today-list { list-style: none; margin: 4px 0 0; padding: 0; }
.asst-today-item {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 8px;
  padding: 6px 4px; border-top: 1px solid var(--line-soft);
  font-size: 13px; color: var(--ink-2); line-height: 1.35;
}
.asst-today-item:first-child { border-top: 0; }
.asst-today-glyph {
  width: 18px; height: 18px; border-radius: 999px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 800; line-height: 1;
  border: 1px solid var(--sev-normal-line); background: var(--sev-normal-bg); color: var(--sev-normal);
}
.asst-today-item.sev-critical .asst-today-glyph { border-color: var(--sev-critical-line); background: var(--sev-critical-bg); color: var(--sev-critical); }
.asst-today-item.sev-warning  .asst-today-glyph { border-color: var(--sev-warning-line); background: var(--sev-warning-bg); color: var(--sev-warning); }
.asst-today-item.sev-info     .asst-today-glyph { border-color: var(--sev-info-line); background: var(--sev-info-bg); color: var(--sev-info); font-style: italic; font-family: Georgia, serif; }
.asst-today-item.sev-critical .asst-today-text { color: var(--ink); font-weight: 600; }
.asst-today-text { min-width: 0; overflow-wrap: anywhere; }
.asst-today-acts { display: inline-flex; gap: 6px; align-items: center; flex: none; }
.asst-today-ask {
  border: 1px solid var(--line); border-radius: 999px; background: var(--card); color: var(--brand-2);
  padding: 4px 11px; font: inherit; font-size: 12.5px; line-height: 1.3; cursor: pointer;
}
.asst-today-ask:hover { background: var(--brand-tint); border-color: var(--brand-2); }
.asst-today-ask:focus-visible, .asst-today-open:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 1px; }
.asst-today-open { font-size: 12.5px; color: var(--brand-2); text-decoration: underline; text-underline-offset: 2px; padding: 4px 2px; }
@media (pointer: coarse) {
  .asst-today-ask { min-height: 44px; padding: 10px 14px; }
  .asst-today-open { min-height: 44px; display: inline-flex; align-items: center; padding: 0 8px; }
  .asst-today-toggle { min-height: 44px; }
}
@media (max-width: 560px) {
  .asst-today { max-height: 30vh; }
  .asst-today-item { grid-template-columns: 20px minmax(0, 1fr); }
  .asst-today-acts { grid-column: 2; justify-content: flex-start; }
  .asst-today-asof { display: none; }
  .asst-today-title { white-space: nowrap; }
}
@media (prefers-reduced-motion: reduce) { .asst-today-chev { transition: none; } }


/* ACCOUNT MENU, 14 Sep 2026 (Alex: "redesign this user profile dropdown
   part"). An initials avatar as the trigger, a profile card at the head of
   the menu, rows with a little more air, Sign Out set apart in red. */
.um-avatar {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 30px; height: 30px; border-radius: 999px;
  background: var(--brand); color: #fff; font-size: 12px; font-weight: 700; letter-spacing: .02em;
}
.um-avatar.lg { width: 40px; height: 40px; font-size: 14px; }
.usermenu > summary { gap: 9px; padding: 4px 8px 4px 4px; border-radius: 999px; min-height: 38px; }
.usermenu > summary:hover { background: var(--bg); }
.usermenu-pop { min-width: 264px; padding: 8px; border-radius: 14px; box-shadow: var(--e-3, 0 12px 28px rgb(13 25 41 / .16)); }
.usermenu-head.um-card {
  display: flex; align-items: center; gap: 12px; padding: 10px 10px 12px; margin: 0 0 6px;
  font-size: inherit; color: inherit; word-break: normal;
}
.um-who { min-width: 0; }
.um-name { margin: 0; font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.25; }
.um-mail { margin: 2px 0 6px; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.usermenu-pop a, .usermenu-pop button { padding: 9px 12px; border-radius: 9px; font-size: 13.5px; }
.usermenu-out { margin-top: 6px; padding-top: 8px; }
.usermenu-out button { color: var(--bad); }
.usermenu-out button:hover { background: var(--bad-tint, #fdecec); color: var(--bad); }
@media (max-width: 640px) { .usermenu > summary { min-width: 44px; padding: 4px; } }

/* Action column: the header sits over its control, on the right (Alex, 14 Sep 2026). */
.tbl-ow th.c-act, .tbl-ow td.c-act { text-align: right; }
.tbl-ow td.c-act .gi { margin-left: auto; }

/* Firm Owners: the Action cell reads left, under its heading (Alex, 14 Sep
   2026: "align Action column buttons to left"). */
.tbl-ow td.c-act.row-actions { text-align: left; }
.tbl-ow td.c-act.row-actions .gi { margin-left: 0; }

/* Firm Owners: subscription dates and days in their own columns (Alex, 14 Sep
   2026). Every .ow-line shares one height, so a two-firm owner's second line
   sits level across Law Firms, Starts, Ends and Days. */
.tbl-ow .ow-line { min-height: 26px; }
.tbl-ow .c-date, .tbl-ow .c-days { white-space: nowrap; }
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow { min-width: 1120px; }
  .tbl-ow .c-owner { width: 15%; }
  .tbl-ow .c-login { width: 18%; }
  .tbl-ow .c-date  { width: 112px; }
  .tbl-ow .c-days  { width: 72px; }
  .tbl-ow .c-act   { width: 96px; }
}

/* ACCOUNT TRIGGER, SECOND PASS (Alex, 14 Sep 2026: "not good enough"). A
   bordered white pill: gradient avatar, name over the seat in two lines, a
   soft chevron. Lifts on hover, presses on click; the seat line and the name
   hide on a phone and the avatar alone remains the target. */
.usermenu > summary {
  gap: 10px; padding: 4px 12px 4px 4px; min-height: 42px;
  background: var(--card); border: 1px solid var(--line); border-radius: 999px;
  box-shadow: var(--e-1);
  transition: box-shadow var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.usermenu > summary:hover { background: var(--card); border-color: #c3d1de; box-shadow: var(--e-2, 0 4px 12px rgb(13 25 41 / .10)); }
.usermenu > summary:active { transform: translateY(1px); box-shadow: var(--e-1); }
.usermenu[open] > summary { border-color: var(--brand-2); box-shadow: var(--ring); }
.um-avatar { width: 32px; height: 32px; background: linear-gradient(135deg, var(--brand-2), var(--brand)); box-shadow: inset 0 0 0 1px rgb(255 255 255 / .15); }
.um-avatar.lg { width: 44px; height: 44px; font-size: 15px; }
.um-trig { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.15; min-width: 0; }
.um-trig .who-name { font-size: 13px; font-weight: 600; color: var(--ink); }
.um-role { font-size: 11px; font-weight: 500; color: var(--muted); letter-spacing: .01em; white-space: nowrap; }
.usermenu-chev { margin-left: 2px; opacity: .45; }
/* THE APPS TRIGGER IS NOT THE ACCOUNT PILL. The pill rule above matches every
   .usermenu > summary, so the Apps square inherited its border, white fill,
   shadow and 999px corners and stood in a box beside a bell that has none
   (Alex, 15 Sep 2026: "no need outline like bell"). Same 36px square, same
   hover tint and same open state as .um-icon-btn, so the two icon controls
   are one thing. */
.usermenu.um-icon > summary {
  width: 36px; min-width: 0; height: 36px; min-height: 0; padding: 0; gap: 0;
  background: none; border: 0; border-radius: var(--radius-s); box-shadow: none;
}
.usermenu.um-icon > summary:hover { background: var(--tint, rgb(13 41 65 / .05)); border-color: transparent; box-shadow: none; color: var(--brand); }
.usermenu.um-icon > summary:active { transform: none; box-shadow: none; }
.usermenu.um-icon[open] > summary { background: var(--tint, rgb(13 41 65 / .05)); border-color: transparent; box-shadow: none; color: var(--brand); }
.usermenu.um-icon > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .usermenu > summary { transition: none; } }
@media (max-width: 640px) {
  .um-trig { display: none; }
  .usermenu > summary { padding: 3px; min-width: 40px; }
  .usermenu-chev { display: none; }
}

/* Firm Owners: the Action heading reads left too (Alex, 14 Sep 2026). */
.tbl-ow th.c-act { text-align: left; }

.tbl-ow .c-mobile { white-space: nowrap; }
@media (min-width: 761px) { .tbl-ow .c-mobile { width: 128px; } .tbl-ow .c-owner { width: 14%; } .tbl-ow .c-login { width: 17%; } }

/* Cells line up exactly under their headings (Alex, 14 Sep 2026). The firm
   link had a 6px margin from when a badge sat before it; the eye glyph is a
   30px box whose icon sits 6px in, so the box is pulled left by the same. */
.tbl-ow .ow-firm { margin-left: 0; }
.tbl-ow td.c-act.row-actions .gi { margin-left: -6px; }
.days-left { font-weight: 600; color: var(--ink); }
.days-left.warn { color: var(--warn); }
.days-left.bad { color: var(--bad); }

/* STALE-TAB NOTICE, REDESIGNED (Alex, 14 Sep 2026). A white card, not an
   alert stripe: refresh glyph in a brand-tinted disc, a bold line and a quiet
   line, Reload and Later. In flow at the top of the page, never fixed. */
.ls-stale {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin: 0 0 16px; padding: 12px 14px 12px 12px;
  border: 1px solid var(--line); border-left-width: 1px; border-radius: 12px;
  background: var(--card); color: var(--ink); box-shadow: var(--e-1);
  font-size: 13.5px; line-height: 1.45;
}
.ls-stale-ic {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 36px; height: 36px; border-radius: 999px; background: var(--brand-tint); color: var(--brand);
}
.ls-stale-t { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 320px; }
.ls-stale-t strong { font-weight: 600; color: var(--ink); }
.ls-stale-t span { font-size: 12.5px; color: var(--muted); }
.ls-stale-acts { display: flex; gap: 8px; margin-left: auto; }
.ls-stale .btn { min-height: 32px; padding: 4px 14px; font-size: 13px; }
.ls-stale-x { margin-left: 0; font-size: 13px; padding: 4px 12px; color: var(--ink-2); }
@media (max-width: 640px) { .ls-stale-acts { margin-left: 0; width: 100%; justify-content: flex-end; } }
.main > .topchrome + .ls-stale { margin: 16px 24px 0; }
@media (max-width: 760px) { .main > .topchrome + .ls-stale { margin: 12px 16px 0; } }

/* The door beside the law firm name on the Firm Owners row (14 Sep 2026). */
.tbl-ow .ow-door { width: 26px; height: 26px; margin-left: 4px; vertical-align: middle; color: var(--brand-2); }
.tbl-ow .ow-door:hover, .tbl-ow .ow-door:focus-visible { background: var(--brand-tint); color: var(--brand); border-color: transparent; }


.tbl-pa .c-act .ow-tools { display: inline-flex; gap: 4px; }
@media (min-width: 761px) { .tbl-pa .c-act { width: 110px; } }
/* Days Left: heading and numbers both right-aligned (Alex, 14 Sep 2026). */
.tbl-ow th.c-days, .tbl-ow td.c-days { text-align: right; }
/* Users and Matters per law firm (14 Sep 2026): right-aligned counts. */
.tbl-ow th.c-count, .tbl-ow td.c-count { text-align: right; white-space: nowrap; }
@media (min-width: 761px) { .tbl-ow .c-count { width: 72px; } .table-scroll table.tbl-stack.tbl-ow { min-width: 1260px; } }
/* Column budget with twelve columns (14 Sep 2026): the law firm name never
   wraps or drops its door; the person and login columns give up the width. */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow { min-width: 1180px; }
  .tbl-ow .c-owner  { width: 12%; }
  .tbl-ow .c-mobile { width: 104px; }
  .tbl-ow .c-login  { width: 15%; }
  .tbl-ow .c-firms  { width: auto; min-width: 230px; }
  .tbl-ow .c-count  { width: 62px; }
  .tbl-ow .c-date   { width: 104px; }
  .tbl-ow .c-days   { width: 82px; }
  .tbl-ow .c-state  { width: 96px; }
  .tbl-ow .c-act    { width: 84px; }
  .tbl-ow .c-firms .ow-line-main { white-space: nowrap; }
  .tbl-ow .ow-email { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* VIEW FIRST, EDIT ON PRESS (14 Sep 2026). Locked fields read as text on a
   quiet ground; Save and Cancel appear only in edit mode, Edit only in view. */
.ls-viewfirst[data-mode="view"] .dialog-body input[readonly],
.ls-viewfirst[data-mode="view"] .dialog-body textarea[readonly],
.ls-viewfirst[data-mode="view"] .dialog-body select:disabled {
  background: var(--bg); border-color: var(--line); color: var(--ink); cursor: default; box-shadow: none; opacity: 1;
}
.ls-viewfirst[data-mode="view"] .dialog-body input[type="password"],
.ls-viewfirst[data-mode="view"] .dialog-body input[type="password"] + .hint,
.ls-viewfirst[data-mode="view"] .dialog-body .vf-edit-only { display: none; }
.ls-viewfirst[data-mode="view"] .dialog-actions button[type="submit"],
.ls-viewfirst[data-mode="view"] .dialog-actions .vf-cancel,
.ls-viewfirst[data-mode="view"] .dialog-actions .own-foot-save { display: none !important; }
.ls-viewfirst[data-mode="edit"] .dialog-actions .vf-edit { display: none !important; }
.ls-viewfirst .vf-edit, .ls-viewfirst .vf-cancel { display: inline-flex; }
.ls-viewfirst[data-mode="view"] .dialog-body .field:has(> input[type="password"]) { display: none; }

/* ===========================================================================
   RECORD DIALOGS, ONE LANGUAGE (14 Sep 2026). Firm Owner, Law Firm and
   Platform Admin on the Admin Console were three different treatments of the
   same thing - a record opened from a list. This layer gives them one:

     h2            the record TYPE (Alex: "title is Law Firm")
     .rd-ident     the record ITSELF: monogram, name, particulars, state pills
     .own-tabs     segmented control where there are tabs
     .dialog-body  .rd-view = fact cards (view first)  |  .rd-form = the form
     .dialog-actions  Close left, Edit or Save right, as everywhere

   The signature of the console's dialogs is the identity band, and it is
   expressed HERE, as a component, never as one-off values on a screen.

   CORNERS ARE ON THE TOKEN SCALE, nothing hand-typed: the dialog itself is
   --radius (16), a card, a tile, a table wrapper or the tab track is --r-md
   (12), a control or a firm monogram --r-sm (8), a chip --r-xs (6), a person's
   monogram or a pill --r-full. A segment inside the 12px tab track with 3px of
   padding is 9px (12 - 3), which is the nested-corner rule, not a fifth size.
   Motion: none that moves layout; the view/edit swap is display only.
   ======================================================================== */
.ls-dialog:not(.assistant) > form > .rd-ident,
.ls-dialog:not(.assistant) > .rd-ident {
  flex: none; display: flex; align-items: center; gap: 14px;
  padding: 2px 56px 14px 24px; min-width: 0;
}
.rd-mono {
  flex: none; display: inline-grid; place-items: center; width: 44px; height: 44px;
  border-radius: var(--r-full); background: var(--brand-tint); color: var(--brand);
  font-size: 15px; font-weight: 700; letter-spacing: .02em; line-height: 1;
}
.rd-firm .rd-mono { border-radius: var(--r-sm); }   /* a practice is a square seal, a person a disc */
.rd-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 0%; }  /* basis 0, or a long particulars line wraps the whole band on a phone */
.rd-who strong { font-size: 16px; font-weight: 600; color: var(--ink); line-height: 1.25; overflow-wrap: anywhere; }
.rd-who span { font-size: 13px; color: var(--muted); line-height: 1.4; overflow-wrap: anywhere; }
.rd-pills { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; flex: none; max-width: 46%; }
.rd-dlg .pill { border-radius: var(--r-full); }
/* The band is the header's last row, so the body's hairline sits right under it. */
.rd-dlg .own-tabs { margin-top: 0; }
.rd-dlg .dialog-body { padding-top: 18px; }

/* Badge tones in the owner and admin dialogs' tables (the shape rule is above). */
.own-dlg .badge.ok   { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-line); }
.own-dlg .badge.warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.own-dlg .badge.bad  { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }
.own-dlg .badge.info { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }
.own-dlg .badge.mute { background: var(--surface-active); color: var(--muted); border-color: var(--line); }
.own-danger { color: var(--bad); border-color: var(--bad-line); }
.own-danger:hover, .own-danger:focus-visible { background: var(--bad-bg); border-color: var(--bad); color: var(--bad); }
.own-ftbl .badge {
  display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: var(--r-xs);
  border: 1px solid transparent; font-size: 10.5px; font-weight: 700; letter-spacing: .05em; white-space: nowrap;
}
/* The Law Firms tab reuses the table's quiet glyph (.gi) outside .tbl-ow. */
.own-ftbl td { vertical-align: top; }
.own-ftbl .ow-tools { display: inline-flex; gap: 2px; flex-wrap: nowrap; }
.own-dlg .gi {
  display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r-sm); background: transparent;
  color: var(--muted); cursor: pointer;
}
.own-dlg .gi svg { width: 17px; height: 17px; }
.own-dlg .gi:hover, .own-dlg .gi:focus-visible { background: var(--surface-active); color: var(--ink); border-color: var(--line); }
.own-dlg .gi.danger { color: var(--bad); }
.own-dlg .gi.danger:hover { background: var(--bad-bg); border-color: var(--bad-line); }
.own-dlg .gi.strong { color: var(--brand-2); }
@media (pointer: coarse) { .own-dlg .gi { width: 44px; height: 44px; } }

/* Section rows: a heading with, at most, one quiet link beside it. */
.rd-sec-h, .own-sec-h { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 10px; }
.rd-sec-h h3, .own-sec-h h3 { margin: 0; font-size: 14px; font-weight: 600; color: var(--ink); text-transform: none; letter-spacing: 0; }
.rd-sec-h a, .own-sec-h a { font-size: 13px; color: var(--brand-2); white-space: nowrap; }
.rd-sec-h a:hover, .own-sec-h a:hover { color: var(--brand); text-decoration: underline; }
.own-panel .own-tblwrap + .own-sec-h,
.own-panel .own-tblwrap + .hint + .own-sec-h { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); }
.rd-dlg .own-sec-sub { margin: -6px 0 12px; }

/* Fact cards: a soft panel, a small-caps heading, a two-column definition
   grid. Two up on a desktop, one on a phone, and a wide card spans both. */
.rd-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.rd-card {
  min-width: 0; padding: 14px 16px 12px; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.rd-card-wide { grid-column: 1 / -1; margin-top: 12px; }
.rd-card > h3, .rd-card-h h3 {
  margin: 0 0 10px; font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.rd-card-h { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.rd-card-h h3 { margin: 0; }
.rd-card-h .btn.small { min-height: 30px; padding: 4px 11px; }   /* corners: .btn's own, never a fourth value */
.rd-card .defs { grid-template-columns: 124px minmax(0, 1fr); row-gap: 7px; column-gap: 12px; font-size: 13.5px; }
.rd-card .defs dt { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 600; padding-top: 2px; }
.rd-card .defs dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; line-height: 1.45; }
.rd-card .defs dd .muted { color: var(--muted); }
.rd-card .defs dd .rd-note { display: block; margin-top: 6px; font-size: 12.5px; line-height: 1.45; }
.rd-card .defs dd .pill { vertical-align: middle; }

/* The facts strip: three small facts along the bottom of the Law Firm dialog. */
.rd-strip {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 12px;
  padding: 12px 16px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--card);
}
.rd-strip > div { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.rd-k { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
.rd-v { font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere; }
.rd-v code { font-size: 12.5px; border-radius: var(--r-xs); }
.rd-foot-hint { margin: 14px 0 0; font-size: 12.5px; }

/* VIEW FIRST. The dialog's data-mode (assets/app.js) chooses which of the two
   renderings of the record is on screen. Both exist in the DOM; the form's
   inputs are what app.js locks, and they are hidden, not disabled, in view. */
.rd-dlg .rd-form { display: none; }
.rd-dlg[data-mode="edit"] .rd-view { display: none; }
.rd-dlg[data-mode="edit"] .rd-form { display: block; }
/* A record dialog that is not view-first (Law Firm) has no form to hide. */
.rd-dlg:not(.ls-viewfirst) .rd-view { display: block; }
/* Edit is a Profile control: the Law Firms tab has nothing it unlocks, and a
   button that does nothing is a dead control. Save stays, because in edit
   mode the one form is still the one form on every tab. */
.own-dlg:has(.own-t-firms:checked) .dialog-actions .vf-edit { display: none !important; }

/* Tiles inside a record dialog sit at card density, and a date or a name in
   a tile is set smaller than a count so it does not wrap in two lines. */
.rd-dlg .own-metrics { margin-bottom: 14px; }
.rd-dlg .own-metrics .metric { padding: 11px 14px 12px; border-radius: var(--r-md); }
.rd-dlg .own-metrics .metric-n { font-size: 20px; }
.rd-dlg .own-metrics .own-metric-date { font-size: 15.5px; letter-spacing: -.01em; overflow-wrap: anywhere; }
.rd-dlg .own-metrics .metric-sub { margin-top: 3px; font-size: 12px; }
.rd-dlg .own-tblwrap { border-radius: var(--r-md); }
/* CORNER BUG FIXED: the head cells carried their own 10px top corners inside
   a 12px wrapper that already clips, which left a notch of white in both top
   corners of every table in these dialogs. The wrapper owns the corners. */
.rd-dlg .own-tblwrap thead th:first-child, .rd-dlg .own-tblwrap thead th:last-child { border-radius: 0; }
.rd-dlg .own-tabs { border-radius: var(--r-md); }
.rd-dlg .own-tabs label { border-radius: calc(var(--r-md) - 3px); }
.rd-dlg .own-tab-n { border-radius: var(--r-full); }
.rd-dlg .own-dlg .gi, .rd-dlg .gi { border-radius: var(--r-sm); }

@media (max-width: 760px) {
  .ls-dialog:not(.assistant) > form > .rd-ident,
  .ls-dialog:not(.assistant) > .rd-ident { padding: 0 56px 12px 16px; gap: 12px; flex-wrap: wrap; }
  .rd-mono { width: 40px; height: 40px; font-size: 14px; }
  .rd-who strong { font-size: 15px; }
  .rd-pills { max-width: none; justify-content: flex-start; width: 100%; padding-left: 52px; }
  .rd-cards, .rd-strip { grid-template-columns: 1fr; }
  .rd-card { padding: 12px 14px 10px; }
  .rd-card .defs { grid-template-columns: 104px minmax(0, 1fr); }
  .rd-card-h { flex-wrap: wrap; }
  .rd-dlg .dialog-body { padding-top: 14px; }
}
/* At 320px a 2 x 2 of tiles leaves 94px for a name, and "Administrator"
   broke mid-word. One column there; a name in a tile wraps at word edges. */
.rd-dlg .own-metrics .own-metric-date { overflow-wrap: break-word; }
@media (max-width: 400px) { .rd-dlg .own-metrics { grid-template-columns: 1fr; gap: 8px; } }

/* NO. COLUMNS ALWAYS READ RIGHT (Alex, 14 Sep 2026: "always align right for all
   No. columns") - heading AND cells, so the ordinal sits under its own word the
   way every other count and money column does. `.c-no` is the class every
   numbered register uses; the issues table on the case record is the one No.
   column without it, so its first column is named here as well. */
.tbl-ow th.c-no, .tbl-ow td.c-no, .tbl-pa th.c-no, .tbl-pa td.c-no,
.tbl-firms th.c-no, .tbl-firms td.c-no, .tbl-owners th.c-no, .tbl-owners td.c-no,
th.c-no, td.c-no { text-align: right; font-variant-numeric: tabular-nums; }

/* The owner's name and its Owner Console door stay on one line (14 Sep 2026):
   the name wraps inside the row, the door sits beside its last line. */
.tbl-ow .c-owner .ow-namerow { display: flex; align-items: center; gap: 2px; min-width: 0; }
.tbl-ow .c-owner .ow-namerow .ow-name { min-width: 0; text-align: left; }
.tbl-ow .c-owner .ow-namerow .ow-door { flex: none; margin-left: 2px; }
/* THE OWNER'S NAME NEVER WRAPS (Alex, 14 Sep 2026: "so many spacing, why name
   go 2 lines?"). The table is fixed-layout, so the owner column was 12% of
   whatever was left after the Law Firm column, and a full Malaysian name with
   its door beside it broke in two. Fixed widths for the owner and the mobile
   columns, sized to the longest plausible content; the Law Firm column stays
   the elastic one and simply gets less. */
@media (min-width: 761px) {
  .tbl-ow .c-owner  { width: 244px; }
  .tbl-ow .c-owner .ow-namerow .ow-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .tbl-ow .c-mobile { width: 124px; }
  .tbl-ow .c-login  { width: 190px; }
  .tbl-ow .c-firms  { width: 250px; }
  /* 48 + 244 + 124 + 190 + 250 + 62 + 62 + 104 + 104 + 82 + 96 + 84 = 1450:
     the floor is the sum of the columns, so nothing can be squeezed under
     its neighbour; narrower screens scroll the register sideways instead. */
  .table-scroll table.tbl-stack.tbl-ow { min-width: 1450px; }
  /* Platform Admins carries tbl-ow for the look, not the twelve-column budget. */
  .table-scroll table.tbl-stack.tbl-ow.tbl-pa { min-width: 960px; }
  .tbl-pa .c-owner { width: 220px; } .tbl-pa .c-mobile { width: 140px; } .tbl-pa .c-login { width: auto; } .tbl-pa .c-date { width: 170px; } .tbl-pa .c-state { width: 110px; }
}

/* ===========================================================================
   OWNER CONSOLE REGISTERS, IN THE ADMIN CONSOLE'S SHAPE (14 Sep 2026). The
   Owner Console's Law Firms and Users tables carry `tbl-ow tbl-reg`: they take
   the Firm Owners register's look (tinted head, link names with a door beside
   them, badges, right-aligned counts and dates, one eye in Action) but NOT its
   column budget, which is written for twelve columns and 1450px. `tbl-reg`
   overrides the widths for its own columns; the min-width is the sum.
   ======================================================================== */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg { min-width: 0; }
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-firms { min-width: 1040px; }
  .tbl-reg .c-no    { width: 48px; }
  .tbl-reg .c-firm  { width: auto; min-width: 220px; }
  .tbl-reg .c-where { width: 130px; }
  .tbl-reg .c-count { width: 72px; }
  .tbl-reg .c-date  { width: 104px; }
  .tbl-reg .c-days  { width: 92px; }
  .tbl-reg .c-setup { width: 128px; }
  .tbl-reg .c-state { width: 104px; }
  .tbl-reg .c-act   { width: 84px; }
  .tbl-reg .c-owner, .tbl-reg .c-mobile, .tbl-reg .c-login, .tbl-reg .c-firms { width: auto; }
}
.tbl-reg .ow-doorform { display: inline; margin: 0; }
.tbl-reg .ow-sub { margin-top: 3px; }
.tbl-reg tr.is-inside > td { background: var(--brand-tint); }
.tbl-reg th.c-days, .tbl-reg td.c-days { text-align: right; white-space: nowrap; }
/* .tbl-firms's older rules set white-space back to normal on every cell; a date must not break at its hyphens. */
.tbl-ow.tbl-reg td.c-date, .tbl-ow.tbl-reg th.c-date, .tbl-ow.tbl-reg td.c-count, .tbl-ow.tbl-reg td.c-state, .tbl-ow.tbl-reg td.c-setup { white-space: nowrap; }
.tbl-ow.tbl-reg td, .tbl-ow.tbl-reg th { padding-left: 12px; padding-right: 12px; vertical-align: middle; }
.tbl-reg td.c-days .ow-sub { text-align: right; }
.tbl-reg .c-setup .badge, .tbl-reg .c-days .badge { white-space: nowrap; }
/* Firm record on the Owner Console: the register table inside a card sits
   flush, and the card's own heading row carries its count. */
.rd-card .table-scroll { margin: 0; }
.rd-card .table-scroll table { background: var(--card); }
.rd-card > .hint:last-child { margin-bottom: 0; }
.rd-card .form-grid { margin-top: 4px; }
.rd-card .banner { margin: 0 0 12px; }
.rd-cards + .banner, .rd-cards + .rd-card, .rd-card-wide + .rd-card-wide, .banner + .rd-card-wide, .rd-card-wide + .banner { margin-top: 12px; }
/* Users register columns (Owner Console). */
@media (min-width: 761px) {
  /* 48 + 190 + 124 + 220 + 280 + 150 + 104 + 150 + 118 = 1384 */
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-users { min-width: 1384px; }
  .tbl-reg .c-person { width: 190px; }
  .tbl-reg.tbl-users .c-mobile { width: 124px; }
  .tbl-reg.tbl-users .c-login  { width: 220px; }
  .tbl-reg .c-seats  { width: 280px; }
  .tbl-reg .c-sub    { width: 150px; }
  .tbl-reg.tbl-users .c-date { width: 150px; }
  .tbl-reg.tbl-users .c-act  { width: 118px; }
}
.tbl-reg .c-seats .ow-line + .ow-line { margin-top: 4px; }
.tbl-reg .c-act .ow-tools { display: inline-flex; gap: 2px; }
.rd-card .rd-pw-grid { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
.rd-card .own-tblwrap { margin: 0; }
.rd-card .own-tblwrap .btn.small { min-height: 28px; padding: 3px 10px; font-size: 12.5px; }
/* The register's count beside the page subtitle, where the table's title bar
   used to carry it (14 Sep 2026). Quiet, one line, links allowed. */
.page-meta { color: var(--muted); }
.page-meta::before { content: "\00b7"; margin: 0 6px; color: var(--muted); }
.page-meta a { color: var(--brand-2); }
.page-actions .page-switch { display: inline-flex; margin: 0; }
.page-actions .page-switch select { min-height: 36px; }
/* Page head without a visible title (14 Sep 2026): the actions row keeps its
   place at the right; the head collapses to the height of its buttons. */
.page-head.page-head-quiet { align-items: center; }
.page-head.page-head-quiet > div:first-child:empty { display: none; }
/* The head's first cell holds only the hidden H1, so space-between left the
   actions in the MIDDLE on a screen that also has the alert bell (Alex,
   14 Sep 2026, the Diary's CSV and Print). Actions push right; the bell
   sits after them at the edge. */
.page-head.page-head-quiet > div:first-child { flex: 0 0 auto; }
.page-head.page-head-quiet .page-actions { margin-left: auto; }
.page-head.page-head-quiet.has-bell .page-actions { margin-right: 12px; }
.page-meta-alone::before { content: none; margin: 0; }
/* PAGE PATH IN THE TOP BAR (14 Sep 2026): left, quiet, the last crumb in ink.
   Takes the space the search box held; the account menu stays right. */
.topbar > .crumbs {
  display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto;
  font-size: 13.5px; color: var(--muted); white-space: nowrap; overflow: hidden;
}
.topbar > .crumbs .crumb { overflow: hidden; text-overflow: ellipsis; }
.topbar > .crumbs .crumb.is-here { color: var(--ink); font-weight: 600; }
.topbar > .crumbs .crumb-sep { color: var(--line); font-weight: 400; }
@media (max-width: 900px) { .topbar > .crumbs { order: 0; flex-basis: 100%; font-size: 13px; } }

/* ===========================================================================
   CONSOLE DASHBOARDS (14 Sep 2026): the Owner Console's and the Admin
   Console's landing screens. Tiles, then two cards side by side (what needs
   you, what just happened), then the register in brief. Same tokens, same
   card, same register table as everywhere else; nothing new is invented.
   ======================================================================== */
.dash-metrics { margin-bottom: 16px; }
@media (min-width: 1100px) { .dash-metrics:has(> :nth-child(6)) { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (max-width: 1099px) and (min-width: 761px) { .dash-metrics:has(> :nth-child(6)) { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.dash-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; margin-bottom: 16px; }
@media (max-width: 1000px) { .dash-grid { grid-template-columns: 1fr; } }
.dash-card { margin-bottom: 0; }
.dash-card > .rd-card-h { margin: 0 0 12px; }
.dash-card > .rd-card-h h3 { font-size: 14px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--ink); }
.dash-card > .rd-card-h a { font-size: 13px; color: var(--brand-2); white-space: nowrap; }
.dash-card > .rd-card-h a:hover { color: var(--brand); text-decoration: underline; }
.card.flush.dash-card > .rd-card-h.dash-tblhead { padding: 14px 18px 0; margin-bottom: 10px; }
.dash-list { list-style: none; margin: 0; padding: 0; }
.dash-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; padding: 9px 0; border-top: 1px solid var(--line-soft); font-size: 13.5px; line-height: 1.45; }
.dash-list li:first-child { border-top: 0; padding-top: 0; }
.dash-list li a { color: var(--brand-2); font-weight: 600; }
.dash-list li a:hover { color: var(--brand); text-decoration: underline; }
.dash-list li .badge { flex: none; }
.dash-list li .muted { flex: 1 1 100%; }
.dash-feed { list-style: none; margin: 0; padding: 0; }
.dash-feed li { display: grid; grid-template-columns: 148px auto minmax(0, 1fr); gap: 10px; align-items: baseline; padding: 8px 0; border-top: 1px solid var(--line-soft); font-size: 13.5px; line-height: 1.45; }
.dash-feed li:first-child { border-top: 0; padding-top: 0; }
.dash-when { color: var(--muted); font-size: 12.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.dash-what { overflow-wrap: anywhere; }
@media (max-width: 640px) { .dash-feed li { grid-template-columns: 1fr; gap: 3px; } }
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-dash { min-width: 880px; }
  .tbl-dash .c-owner { width: 200px; }
  .tbl-dash .c-firm { width: auto; }
}
.dash-card .empty { padding-top: 6px; padding-bottom: 6px; }
/* The firm's name rides on the account menu's second line (14 Sep 2026). */
.um-role .um-firm { color: var(--line); }
.um-role .um-firm-name { color: var(--muted); }
@media (max-width: 640px) { .um-role .um-firm, .um-role .um-firm-name { display: none; } }
/* AI LAWYERS (14 Sep 2026): a wider record dialog, the master switch as a
   wide card, the feature groups as cards two up. Checkboxes locked in view
   mode read as facts, not as a disabled form. */
.ls-dialog.xwide { width: min(1120px, 100vw - 32px); }
.ai-master .checkline { margin: 0; }
.ai-master .hint.irreversible { margin-top: 10px; }
.ai-groups { margin-top: 12px; }
.ai-groups .rd-card > h3 { margin-bottom: 6px; }
.ai-groups .checkline { padding: 8px 0; border-top: 1px solid var(--line-soft); margin: 0; }
.ai-groups .checkline:first-of-type { border-top: 0; }
.ai-groups .checkline label, .ai-groups .checkline strong { font-weight: 600; color: var(--ink); }
.ai-groups .checkline .hint { margin-top: 2px; }
.ls-viewfirst[data-mode="view"] .dialog-body input[type="checkbox"]:disabled { opacity: 1; accent-color: var(--brand); cursor: default; }
.tbl-reg.tbl-ai .c-firm { width: auto; }
@media (min-width: 761px) { .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-ai { min-width: 760px; } .tbl-ai .c-count { width: 118px; } }
/* USER TYPES (14 Sep 2026): the grant grid inside the type record, and the
   note under a flush register. */
.card.flush .tbl-note { margin: 0; padding: 12px 18px 16px; border-top: 1px solid var(--line-soft); font-size: 13px; line-height: 1.5; color: var(--muted); }
.ut-module { margin-top: 12px; }
.ut-module:first-of-type { margin-top: 0; }
.ut-grid th.ut-opt, .ut-grid td.ut-opt { text-align: center; width: 120px; }
.ut-grid td { vertical-align: top; }
.ut-radio { display: inline-flex; align-items: center; justify-content: center; min-width: 32px; min-height: 28px; cursor: pointer; }
.ut-radio input { width: 18px; height: 18px; accent-color: var(--brand); margin: 0; }
.ls-viewfirst[data-mode="view"] .ut-radio input:disabled { opacity: 1; cursor: default; }
.ls-viewfirst[data-mode="view"] .ut-radio { cursor: default; }
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-types { min-width: 860px; }
  .tbl-types .c-type { width: auto; }
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-admins { min-width: 820px; }
  .tbl-admins .c-person { width: 220px; } .tbl-admins .c-login { width: auto; }
}
.subnav + .card, .dash-metrics + .subnav { margin-top: 14px; }
.dash-card + .dash-card { margin-top: 16px; }
.dash-grid > .dash-card + .dash-card { margin-top: 0; }  /* side by side in the grid: the sibling gap is the grid's */
/* THE DOOR STAYS ON THE FIRM'S LINE (Alex, 14 Sep 2026: "the flaticon always
   after the firm name, not another line or row"). The name ellipsises before
   the door ever drops; the form the door sits in is inline. */
.tbl-ow .c-firms .ow-line-main { display: flex; align-items: center; gap: 4px; min-width: 0; white-space: nowrap; }
.tbl-ow .c-firms .ow-line-main .ow-firm { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tbl-ow .c-firms .ow-line-main .ow-doorform { display: inline-flex; flex: none; margin: 0; }
.tbl-ow .c-firms .ow-line-main .ow-door, .tbl-ow .c-firms .ow-line-main .badge { flex: none; }
@media (min-width: 1100px) { .dash-metrics:has(> :nth-child(5):last-child) { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
/* ASK LAWSERVA, FLOATING (Alex, 14 Sep 2026). Bottom right, clear of the
   safe area; the page under it reserves the pill's height plus breathing
   room so nothing is ever covered. Brand fill so it reads as the one door to
   the assistant; no glyph. Out of the way on print and in overlay frames. */
.askfab {
  position: fixed; right: 24px; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); z-index: 60;
  min-height: 44px; padding: 0 20px; border-radius: var(--r-full);
  background: var(--brand); color: #fff; border: 1px solid var(--brand);
  font-size: 14px; font-weight: 600; letter-spacing: .01em;
  box-shadow: 0 6px 18px rgb(15 28 48 / .22), 0 1px 2px rgb(15 28 48 / .18);
}
.askfab:hover { background: var(--brand-2, var(--brand)); border-color: var(--brand-2, var(--brand)); color: #fff; }
.askfab:focus-visible { outline: none; box-shadow: var(--ring), 0 6px 18px rgb(15 28 48 / .22); }
.shell.has-askfab .page { padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
@media (max-width: 900px) {
  .askfab { right: 16px; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
  .shell.has-askfab .page { padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px)); }
}
.shell-modal .askfab, .askfab[hidden] { display: none !important; }
@media print { .askfab { display: none !important; } }
/* CALENDAR LEGEND, no glyphs (Alex, 14 Sep 2026: "do not use emoji or
   symbol"). A swatch in the severity ink beside the words; the chip and the
   event heading carry no glyph either. */
.cal-legend .cal-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; background: currentColor; flex: none; }
.cal-legend .sev-unknown .cal-swatch { background: transparent; border: 1.5px solid currentColor; }
.cal-legend .sev-normal .cal-swatch { opacity: .55; }
/* LAW FIRMS register, column budget (Alex, 14 Sep 2026: "so much space ...
   the right side is almost touching the edge"). Stated last so it wins over
   the .tbl-ow and .tbl-firms budgets above: the name takes a quarter, Where
   takes what is left, every counted column holds its own width, and the
   three tools get a column wide enough to sit clear of the edge. */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-firms { min-width: 1040px; }
  .tbl-ow.tbl-firms .c-firm  { width: auto; min-width: 0; }
  .tbl-ow.tbl-firms .c-where { width: 150px; }
  .tbl-ow.tbl-firms .c-count { width: 84px; }
  .tbl-ow.tbl-firms .c-date  { width: 116px; }
  .tbl-ow.tbl-firms .c-days  { width: 96px; min-width: 0; }
  .tbl-ow.tbl-firms .c-state { width: 104px; }
  .tbl-ow.tbl-firms .c-act { width: 132px; min-width: 0; padding-right: 18px; }
}
.tbl-ow.tbl-firms .c-act .ow-tools { justify-content: flex-end; }
/* THE ASSISTANT COMPOSER, one pill (Alex, 14 Sep 2026: "this part of the ask
   ai is badly designed"). The pill carries the border and the focus ring; the
   textarea inside it carries neither, so there is no box inside a box. Attach
   and Send are the same 36px circle at either end, level with the text. */
.composer { padding: 5px 5px 5px 6px; border-radius: 999px; }
.composer:has(#asst_q:not(:placeholder-shown)) { border-radius: 22px; }
.composer-row { align-items: center; gap: 4px; }
.composer textarea, .composer textarea:focus {
  border: 0; box-shadow: none; outline: none; background: transparent; border-radius: 0;
  padding: 8px 6px; min-height: 36px; line-height: 20px; resize: none; font-size: 14px;
}
.composer-attach, .composer-send { width: 36px; height: 36px; }
.composer-attach svg, .composer-send svg { width: 18px; height: 18px; display: block; }
.composer-attach { color: var(--muted); }
.composer-attach:hover { color: var(--brand); background: var(--brand-tint); }
.composer-send:disabled { background: var(--line); color: var(--card); }
.asst-composer-note, .composer + .hint { margin: 8px 6px 0; }
@media (pointer: coarse) { .composer-attach, .composer-send { width: 44px; height: 44px; } }
/* RECORD DIALOGS WITH TABS (14 Sep 2026): Law Firm (Details, Subscriptions),
   Platform Admin (Details, Activity). Details opens view first and owns Edit,
   Save and Cancel; the other tab is read-only, so the footer drops to Close. */
.own-dlg:has(.own-t-details:checked) .p-details,
.own-dlg:has(.own-t-subs:checked) .p-subs,
.own-dlg:has(.own-t-activity:checked) .p-activity { display: block; }
.own-dlg:has(.own-t-subs:checked) .dialog-actions .vf-edit,
.own-dlg:has(.own-t-subs:checked) .dialog-actions .vf-cancel,
.own-dlg:has(.own-t-subs:checked) .dialog-actions button[type="submit"],
.own-dlg:has(.own-t-activity:checked) .dialog-actions .vf-edit,
.own-dlg:has(.own-t-activity:checked) .dialog-actions .vf-cancel,
.own-dlg:has(.own-t-activity:checked) .dialog-actions button[type="submit"] { display: none !important; }
.own-dlg.fv-dlg .rd-card-h .btn.small { margin-left: auto; }
.own-dlg.fv-dlg .rd-card-h .btn.small + .muted, .own-dlg.fv-dlg .rd-card-h .muted + .btn.small { margin-left: 10px; }
.tbl-firms .ow-tools .ow-doorform { display: inline-flex; margin: 0; }
/* RESERVED AUTHORITY register (14 Sep 2026): one row per person per act,
   standing in words. The row filter sits in the card head, right of the
   title, as a subnav with no top margin of its own. */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-ra { min-width: 1040px; }
  .tbl-ra .c-type { width: 190px; } .tbl-ra .c-act-name { width: auto; } .tbl-ra .c-class { width: 120px; }
  .tbl-ra .c-auth { width: 120px; } .tbl-ra .c-standing { width: 190px; } .tbl-ra .c-since { width: 122px; white-space: nowrap; }
}
.tbl-ra td.c-act-name { white-space: normal; overflow-wrap: anywhere; }
.rd-card-h .subnav.ra-show { margin: 0; }
.rd-card-h .subnav.ra-show a { padding: 4px 10px; font-size: 12.5px; }
/* ACTIVITY register (14 Sep 2026): dated rows, the event as the elastic column. */
@media (min-width: 761px) {
  .table-scroll table.tbl-stack.tbl-ow.tbl-reg.tbl-activity { min-width: 980px; }
  .tbl-activity .c-date { width: 150px; } .tbl-activity .c-from { width: 170px; } .tbl-activity .c-who { width: 170px; }
  .tbl-activity .c-state { width: 120px; } .tbl-activity .c-what { width: auto; }
}
.tbl-activity td.c-what { white-space: normal; overflow-wrap: anywhere; }
.tbl-activity .badge.mute { background: var(--surface-active); color: var(--muted); border-color: var(--line); }
/* ONE BADGE EVERYWHERE (14 Sep 2026). The chip was styled only inside the
   register tables and the owner dialog, so on the dashboards' attention lists
   it rendered as bare uppercase text on a grey smear. Base shape and tones now
   apply wherever .badge is written; the scoped rules above still win inside
   their tables. */
.badge {
  display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: var(--r-xs);
  border: 1px solid transparent; font-size: 10.5px; font-weight: 700; letter-spacing: .05em; line-height: 1;
  white-space: nowrap; background: var(--surface-active); color: var(--muted); border-color: var(--line);
}
.badge.lg { height: 22px; padding: 0 9px; font-size: 11px; }
.badge.ok   { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-line); }
.badge.warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.badge.bad  { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }
.badge.info { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }
.badge.mute { background: var(--surface-active); color: var(--muted); border-color: var(--line); }
/* The two dashboard cards share one padding and one heading row, whatever
   sits inside them, so their headings align across the grid. */
.dash-grid > .dash-card { padding: 18px; align-self: stretch; }
.dash-grid > .dash-card > .rd-card-h { min-height: 24px; }
.dash-list li a { background: transparent; }

/* =========================================================================
   CORNERS, SQUARED DOWN (Alex, 14 Sep 2026: "i dont like too much round
   radius ... please redesign for all"). One scale, stated at the end so it
   wins over every earlier value, tokens and hand-typed alike. Direction from
   ui-ux-pro-max (Trust and Authority: restrained, corporate, dense) and the
   Tailwind v4 radius scale (4 / 6 / 8 / 10). Nesting still steps DOWN.

     --r-xs    4px   chips, badges, pills, counts, a segment inside a track
     --r-sm    6px   controls: button, input, select, row hover, menu item
     --r-md    8px   surfaces: card, tile, table wrapper, tab track
     --radius 10px   dialogs, sheets, popovers
     --r-full        AVATARS, MONOGRAMS AND DOTS ONLY. Nothing else is round.
   ========================================================================= */
:root {
  --r-xs: 4px; --r-sm: 6px; --r-md: 8px;
  --radius: 10px; --radius-s: 6px; --radius-lg: 8px; --radius-m: 10px;
}
/* Surfaces */
.card, .metric, .rd-card, .own-tblwrap, .table-scroll, .cal-legend, .banner, .empty, .asst-file,
.ls-sheet .ls-note, .ls-stale { border-radius: var(--r-md); }
.ls-dialog, .ls-sheet, .ls-sheet.rec-alert-panel, .usermenu-pop, .composer,
.composer:has(#asst_q:not(:placeholder-shown)) { border-radius: var(--radius); }
/* Controls */
.btn, .gsearch input, input[type="text"], input[type="email"], input[type="password"], input[type="tel"],
input[type="date"], input[type="datetime-local"], input[type="number"], input[type="search"], select, textarea,
.side .sidenav a, .usermenu-pop a, .usermenu-pop button, .usermenu > summary, .askfab,
.composer-attach, .composer-send, .asst-file-x, .ls-sheet-x, .ls-sheet .alert-jump-btn, .asst-today-ask,
.rec-bell, .dialog-x, .tbl-ow .gi, .brand-mark { border-radius: var(--r-sm); }
.own-tabs, .dlg-tabs, .segmented, .avail { border-radius: var(--r-md); }
.own-tabs label, .dlg-tabs label, .segmented > *, .subnav a, .asst-chip, .asst-eg { border-radius: var(--r-xs); }
/* Badges, pills, counts: squared chips, never capsules */
.pill, .badge, .card-head .count, .own-tab-n, .rd-dlg .pill, .rd-dlg .own-tab-n, .navcount,
.rec-bell-tool .rec-bell-n, .subnav a .chipcount, .subnav.fb-chips button .chipcount, .ls-note-ico,
.cal-month td.is-today .cal-daynum, .days-left { border-radius: var(--r-xs); }
/* Still round: people and dots */
.um-avatar, .rd-mono, .usermenu-dot, .visiting-dot, .aibox-dot, .ls-stale-ic, .asst-today-glyph,
.check .mark, .btn[aria-busy="true"]::after { border-radius: 50%; }
.rd-firm .rd-mono { border-radius: var(--r-sm); }
/* THE FIRM'S FRONT DOOR (firm.php, 14 Sep 2026): the sign-in page's shell
   holding a letterhead and two doors. */
.auth .auth-card .defs { margin-bottom: 16px; }
.fd-doors { display: flex; gap: 10px; flex-wrap: wrap; }
.fd-doors .btn { flex: 1 1 0; justify-content: center; }
/* The live name check's verdict, in the field's own hint. */
.field .hint[data-slug-preview].is-ok { color: var(--ok); }
.field .hint[data-slug-preview].is-bad { color: var(--bad); }
.fm-card .badge { flex: none; }
/* GLYPH ACTION BUTTONS EVERYWHERE (Alex, 14 Sep 2026: "do not use these
   kinds of action buttons at the action column, always use flaticon only").
   .gi used to be styled only inside the console registers (.tbl-ow .gi); the
   same control now serves every table in the application. Words live in
   aria-label and title, never in the cell. */
.gi {
  display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r-sm); background: transparent;
  color: var(--muted); cursor: pointer; text-decoration: none; vertical-align: middle;
}
.gi svg { width: 17px; height: 17px; }
.gi:hover, .gi:focus-visible { background: var(--surface-active); color: var(--ink); border-color: var(--line); }
.gi:focus-visible { outline: none; box-shadow: var(--ring); }
.gi.danger { color: var(--bad); }
.gi.danger:hover, .gi.danger:focus-visible { background: var(--bad-bg); border-color: var(--bad-line); }
.gi.strong { color: var(--brand-2); }
.gi[disabled], .gi[aria-disabled="true"] { opacity: .4; cursor: default; }
td .gi + .gi, td .gi + form, td form + .gi, td form + form { margin-left: 2px; }
td .d-inline .gi { vertical-align: middle; }
@media (pointer: coarse) { .gi { width: 44px; height: 44px; } }
/* Filter bars: the button sits level with the fields beside it (Alex, 14 Sep
   2026: "adjust filter height, to align"). Fields are 44px tall in a form
   row; the Filter button was 36px and floated above the baseline. */
.toolbar .field > .btn { min-height: 42px; }
.toolbar .field:has(> .btn:only-child) { align-self: flex-end; }
.tbl-firms .ow-namerow { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; }
.tbl-firms .ow-namerow .badge { flex: none; }
/* RECORD DIALOG RHYTHM (Alex, 14 Sep 2026: "do not container stick
   together"). Every block inside a record's view - tiles, banners, cards,
   card grids, hints - keeps 12px from the one above it, whatever order a
   screen puts them in; the ad hoc sibling pairs above stop being the only
   thing standing between two containers. */
.rd-dlg .rd-view > * + *, .rd-dlg .own-panel > * + * { margin-top: 12px; }
.rd-dlg .rd-view > .own-metrics { margin-bottom: 0; }
.rd-dlg .rd-view > .rd-cards + .rd-card-wide, .rd-dlg .rd-view > .rd-card-wide + .rd-cards { margin-top: 12px; }
.banner .banner-list { margin: 8px 0 0; padding-left: 18px; font-size: 13.5px; }
.banner .banner-list li + li { margin-top: 2px; }
.rd-dlg .rd-view > .banner, .rd-dlg .own-panel > .banner { margin-bottom: 0; }
/* THE FIRM'S MAILBOX (14 Sep 2026): unread rows carry weight, the message
   text is a pre-wrapped block inside its card, and the sent register's
   origin reads as a badge. */
tr.is-unread td { font-weight: 600; }
tr.is-unread td .muted, tr.is-unread td .t-sm { font-weight: 400; }
.mail-body { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14px; line-height: 1.55; padding: 12px 14px; margin-top: 10px;
  background: var(--card); border: 1px solid var(--line-soft); border-radius: var(--r-sm); max-height: 60vh; overflow: auto; }
.rd-dlg .rd-card .mail-body { background: var(--card); }
#dlg_compose textarea { min-height: 220px; }
#cmp_docs { min-height: 120px; }

/* ===========================================================================
   ASK AI LAWYER, TWO PANES. 14 Sep 2026 (Alex: "chat history in the left,
   new chat also, compact and modern").
   The dialog is a grid: a conversations rail on the left, the chat on the
   right, the Close footer across both. Fixed head, fixed foot, only the log
   scrolls - the modal rule holds; the X stays top right, Close bottom left.
   Every earlier .ls-dialog.assistant > child rule is superseded here because
   the children moved under .asst-main.
   ======================================================================== */
.ls-dialog.assistant[open] {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  width: min(1080px, 96vw);
  padding: 0;
  overflow: hidden;
}
.ls-dialog.assistant > .asst-rail {
  grid-row: 1; grid-column: 1;
  display: flex; flex-direction: column; min-height: 0;
  background: var(--bg); border-right: 1px solid var(--line);
}
.ls-dialog.assistant > .asst-main {
  grid-row: 1; grid-column: 2;
  display: flex; flex-direction: column; min-height: 0; min-width: 0;
  background: var(--card);
}
.ls-dialog.assistant > .dialog-actions {
  grid-row: 2; grid-column: 1 / -1;
  margin: 0; padding: 12px 22px; border-top: 1px solid var(--line); background: var(--card);
}
.ls-dialog.assistant > .dialog-x { top: 14px; right: 16px; }

/* The rail. */
.asst-rail-top { flex: 0 0 auto; padding: 14px 12px 8px; display: flex; flex-direction: column; gap: 8px; }
.asst-new {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 38px; padding: 0 12px;
  border: 1px solid var(--brand); border-radius: var(--radius-s);
  background: var(--brand); color: #fff; font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
}
.asst-new:hover { filter: brightness(1.08); }
.asst-new:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.asst-new svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.asst-rail-toggle { display: none; }
.ls-dialog.assistant .asst-threads {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  border: 0; border-radius: 0; background: none; padding: 4px 8px 14px; margin: 0; max-height: none;
}
.ls-dialog.assistant .asst-threads-head { padding: 6px 8px 4px; margin: 0; }
.ls-dialog.assistant .asst-thread {
  flex-direction: column; align-items: stretch; gap: 2px; padding: 8px 10px;
  border-radius: var(--radius-s); color: var(--ink-2);
}
.ls-dialog.assistant .asst-thread:hover { background: var(--card); color: var(--ink); }
.ls-dialog.assistant .asst-thread[aria-current="true"] {
  background: var(--card); color: var(--ink); box-shadow: inset 3px 0 0 var(--brand);
}
.ls-dialog.assistant .asst-thread-t { font-size: 13px; font-weight: 500; }
.ls-dialog.assistant .asst-thread-w { font-size: 11.5px; }
.ls-dialog.assistant .asst-threads-empty { padding: 6px 10px; }

/* The chat side: a compact one-line head, then the parts that give. */
/* ONE LINE (Alex, 14 Sep 2026: "more compact, do not waste space, less
   words"): the title and a short muted note on the same baseline, 44px tall,
   the X centred on it. */
.ls-dialog.assistant .asst-head {
  flex: 0 0 auto; min-height: 44px; padding: 0 64px 0 22px;
  display: flex; align-items: center;
  border-bottom: 1px solid var(--line-soft, var(--line));
}
.ls-dialog.assistant .asst-head > div { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.ls-dialog.assistant .asst-head h2 { font-size: 15px; margin: 0; }
.ls-dialog.assistant .asst-head .dialog-sub { font-size: 12.5px; margin: 0; color: var(--muted); }
.ls-dialog.assistant > .dialog-x { top: 4px; right: 12px; }
.ls-dialog.assistant .asst-log { padding-top: 12px; }
.ls-dialog.assistant .asst-scope { flex: 0 0 auto; margin: 10px 22px 0; }
.ls-dialog.assistant .asst-today { flex: 0 0 auto; margin: 10px 22px 0; }
.ls-dialog.assistant .asst-log { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 22px; }
.ls-dialog.assistant form.asst-form { flex: 0 0 auto; padding: 8px 22px 10px; border-top: 1px solid var(--line-soft, var(--line)); }
.ls-dialog.assistant .asst-foot { margin: 8px 0 0; font-size: 12px; }

/* Below 720px the rail folds: New Chat and a Conversations toggle on one
   row, the list dropping down under them when opened. */
@media (max-width: 720px) {
  .ls-dialog.assistant[open] { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; width: min(100vw - 16px, 1080px); }
  .ls-dialog.assistant > .asst-rail { grid-row: 1; grid-column: 1; border-right: 0; border-bottom: 1px solid var(--line); }
  .ls-dialog.assistant > .asst-main { grid-row: 2; grid-column: 1; }
  .ls-dialog.assistant > .dialog-actions { grid-row: 3; }
  .asst-rail-top { flex-direction: row; padding: 10px 12px; padding-right: 64px; }
  .asst-new { width: auto; flex: 1 1 auto; }
  .asst-rail-toggle {
    display: inline-flex; align-items: center; min-height: 38px; padding: 0 12px;
    border: 1px solid var(--line); border-radius: var(--radius-s); background: var(--card);
    color: var(--ink-2); font: inherit; font-size: 13px; cursor: pointer;
  }
  .asst-rail-toggle[aria-expanded="true"] { color: var(--brand); border-color: var(--brand); }
  .ls-dialog.assistant .asst-threads { display: none; max-height: 40vh; }
  .ls-dialog.assistant .asst-threads.is-open { display: block; }
  .ls-dialog.assistant > .dialog-x { top: 10px; right: 12px; }
}

/* CARDS DO NOT TOUCH (Alex, 14 Sep 2026: "do not stick together"). On the
   User record the Seats card sits straight in .dialog-body above the .rd-cards
   pair, and nothing put air between them. Any card or card grid that follows
   another in a record dialog's body now keeps 14px, the same as the grid gap. */
.rd-cards { gap: 14px; }
.rd-dlg .dialog-body > .rd-card + .rd-cards,
.rd-dlg .dialog-body > .rd-cards + .rd-card,
.rd-dlg .dialog-body > .rd-card + .rd-card,
.rd-dlg .dialog-body > .rd-cards + .rd-cards { margin-top: 14px; }
.rd-dlg .dialog-body > .rd-card.rd-card-wide:first-child { margin-top: 0; }

/* LETTERS AND CALLS REGISTER (14 Sep 2026). One row, one line of facts. */
.co-tbl td, .co-tbl th, td.co-main, td.co-date { vertical-align: top; }
.co-tbl .co-date { white-space: nowrap; font-variant-numeric: tabular-nums; }
.co-tbl .co-main { min-width: 280px; }
.co-subject { font-weight: 600; color: var(--ink); line-height: 1.35; }
.co-party { font-size: 12.5px; color: var(--ink-2); margin-top: 2px; }
.co-sum { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.4; }
.co-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.co-tag {
  display: inline-flex; align-items: center; gap: 4px; padding: 1px 7px;
  border: 1px solid var(--line); border-radius: var(--r-xs); background: var(--bg);
  font-size: 11.5px; color: var(--ink-2); max-width: 100%;
}
.co-tag svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.co-tag-wp { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }
.co-tag-ai { color: var(--brand); border-color: var(--brand-tint); background: var(--brand-tint); }
.co-dir { white-space: nowrap; font-size: 12.5px; color: var(--ink-2); }
.co-dir-arrow { font-weight: 700; }
.co-dir-out { color: var(--brand); }
.co-med { white-space: nowrap; }
.co-tbl .co-matter a { font-weight: 600; }
.co-tbl .co-by { white-space: nowrap; }
/* Three families at a glance: letters plain, attendance notes tinted, email
   in the brand tint (lawyer review, 15 Sep 2026). Words carry it; the tint
   only groups. */
.co-med-call, .co-med-meeting { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
.co-med-email { background: var(--brand-tint); color: var(--brand); border-color: transparent; }
.co-med-registered, .co-med-ar_registered, .co-med-hand { font-weight: 600; }

/* THE ICON POPOVERS BELOW 700px OPEN TO THE RIGHT, NOT OFF THE LEFT EDGE.
   At that width .topbar > .who is pinned to the left edge (rule above, "Left,
   not right"), but every .usermenu-pop is anchored right:0 to its summary, so
   a 360px Apps popover opened from a 40px control at x=24 laid itself out at
   x=-279 with most of its grid outside the viewport (UI audit, 15 Sep 2026;
   latent while the popover was empty). Same fix .rec-bell-panel already
   carries. The account menu at the far right keeps its right anchor. */
@media (max-width: 700px) {
  .um-icon .usermenu-pop { right: auto; left: 0; }
  .um-apps .um-icon-pop { width: min(360px, calc(100vw - 24px)); }
}

/* ===========================================================================
   SEARCHABLE PICKERS - .ls-pick (15 Sep 2026). See "SEARCHABLE PICKERS" in
   app.js. The box is a plain text input so it inherits every field rule
   above; the select behind it is kept in the form but taken out of sight.
   The list is a small card: --r-md corner, --e-3 shadow, brand mark on the
   matched letters, --line-soft on the active row. Fixed-positioned by script
   so it escapes a scrolling dialog body.
   ======================================================================== */
.ls-pick { position: relative; }
.ls-pick > select {
  position: absolute; inset: 0; width: 1px; height: 1px; opacity: 0;
  pointer-events: none; margin: 0; padding: 0; border: 0;
}
.ls-pick .ls-pick-in { width: 100%; padding-right: 64px; }
.ls-pick:not(.has-value) .ls-pick-in { padding-right: 40px; }
.ls-pick-tog, .ls-pick-clear {
  position: absolute; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border: 0; background: transparent; color: var(--muted);
  border-radius: var(--r-xs); cursor: pointer; padding: 0;
}
.ls-pick-tog { right: 6px; }
.ls-pick-clear { right: 34px; font-size: 18px; line-height: 1; }
.ls-pick-tog:hover, .ls-pick-clear:hover { background: var(--line-soft); color: var(--ink); }
.ls-pick-clear:focus-visible, .ls-pick-tog:focus-visible { outline: none; box-shadow: var(--ring); }
.ls-pick-in[aria-expanded="true"] { border-color: var(--brand-2); }

.ls-pick-pop {
  position: fixed; z-index: 90; overflow: auto; overscroll-behavior: contain;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--e-3); padding: 6px;
}
.ls-pick-pop ul { list-style: none; margin: 0; padding: 0; }
.ls-pick-pop li[role="option"] {
  padding: 8px 12px; border-radius: var(--r-sm); cursor: pointer;
  font-size: 14px; line-height: 1.35; color: var(--ink);
  display: flex; align-items: center; gap: 8px;
}
.ls-pick-pop li[role="option"].is-active { background: var(--line-soft); }
.ls-pick-pop li[role="option"][aria-selected="true"] { font-weight: 600; }
.ls-pick-pop li[role="option"][aria-selected="true"]::after {
  content: ""; margin-left: auto; width: 8px; height: 8px; border-radius: var(--r-full);
  background: var(--brand-2); flex: 0 0 auto;
}
.ls-pick-pop mark { background: transparent; color: var(--brand-2); font-weight: 600; }
.ls-pick-pop .ls-pick-grp {
  padding: 10px 12px 4px; font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.ls-pick-pop .ls-pick-none { padding: 12px; color: var(--muted); font-size: 13px; }
@media print { .ls-pick-tog, .ls-pick-clear, .ls-pick-pop { display: none; } }

/* ===========================================================================
   NEEDS YOUR ATTENTION - TILES (15 Sep 2026). Replaces the left-bordered
   stack that sat in a 78ch column with the right half of the card empty and
   nothing between one finding and the next. Overrides the .attn rules above
   by source order.
   ======================================================================== */
.attn-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: 14px; padding: 0 18px 18px;
}
@media (max-width: 480px) { .attn-grid { grid-template-columns: 1fr; padding: 0 14px 14px; } }
.attn-grid .attn {
  display: flex; flex-direction: column; gap: 8px; margin: 0;
  padding: 16px 18px 14px 20px;
  background: var(--card); border: 1px solid var(--line); border-left-width: 4px;
  border-radius: var(--r-md);
}
.attn-grid .attn-critical { border-left-color: var(--bad);  background: linear-gradient(90deg, rgb(185 28 28 / .04), transparent 40%); }
.attn-grid .attn-warning  { border-left-color: var(--warn); background: linear-gradient(90deg, rgb(180 83 9 / .05), transparent 40%); }
.attn-grid .attn-info     { border-left-color: var(--brand-2); }
.attn-grid .attn-h { margin: 0; align-items: center; gap: 10px; font-size: 14.5px; }
.attn-grid .attn-h strong { line-height: 1.3; }
.attn-grid .attn-d { max-width: none; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }
.attn-grid .attn-a { margin-top: auto; padding-top: 6px; border-top: 1px solid var(--line-soft); font-size: 13px; }
.attn-grid .attn-a a { display: inline-flex; align-items: center; gap: 6px; }
.attn-grid .attn-a a::after { content: "\2192"; font-weight: 400; }
