From d33785789ba905b6fed8be2937efbb9756e81b91 Mon Sep 17 00:00:00 2001 From: MrChengLen Date: Thu, 29 Oct 2026 16:03:09 +0100 Subject: [PATCH] =?UTF-8?q?feat(a11y):=20text=20reaches=20WCAG=20AA=20on?= =?UTF-8?q?=20every=20dark=20page=20=E2=80=94=20and=20the=20tools=20speak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit About 40 % of the text on the home and /tools pages fell below 4.5 : 1: raw text-gray-500 (4.16), text-gray-600 (2.3–2.7), indigo text on cards (3.9), white on the indigo buttons (4.47) and on the green download button (3.8, hover 2.5). The fix runs through the design tokens, so every page follows from one place: - ink.faint #6b7280 → #8b929e (≥ 4.69 : 1 even on gray-800), and the raw greys in templates and JS now use it; decorative upload icons and disabled buttons take ink-dim (disabled still looks disabled). - Brand-coloured text uses brand-light; filled buttons use brand-dark with a new brand-deep hover (white on them 6.3 / 7.9 : 1). The logo, icons and focus ring keep brand. Download buttons emerald-700/800. - Raw inputs and selects get the components' focus ring instead of a 1 px border change, and every form field uses 16 px text on phones, so iOS stops zooming in. Screen readers: one always-present polite live region (#fm-live) and announce.js. The converter, the PDF tools and the redaction tool announce the progress text and the result card as shown (download label, hints, batch summary); error texts are role="alert", and every error path empties the region first. Only text already on screen is read out. A skip link ("Skip to main content") is the first Tab stop on every page except the § 312k /cancel page; the quality slider and the target-size input are named by their headings; the compress-mode buttons show and announce which one is selected (a grey background covered the highlight); the account button has a name, aria-expanded, opens on keyboard focus and closes with Escape; the mobile-menu entries for the dashboard and sign-out get the same tap height as the rest. Found in review and fixed here, because the skip link would trigger them: the sign-in, registration and password pages submitted on Enter anywhere on the page — now only from a field — and the password-reset page showed "email sent" whatever the server answered; it now checks the field and the response. The /formats search keeps its #q= when the skip link changes the hash. The batch download label counts the files in the ZIP (X-FileMorph-Batch-Succeeded), not the selected ones. Guards: §13 of test_public_claims checks every text token against all dark surfaces from tailwind.config.js and bans the low-contrast greys, brand-coloured text and the white-on-brand/emerald-600 fills in templates and JS; test_accessibility pins the skip link, the live region, the alerts and the input names. The admin cockpit's colour keys and the aria-hidden gray-700 separators stay as they are. scripts/make_testdata_a11y_preview.py writes the photo for the manual check. Full suite 3082 green; ruff + i18n-drift clean; Tailwind rebuilt; requirements untouched. Co-Authored-By: Claude Opus 5.5 --- app/static/css/tailwind.3e9d4c71.css | 1 + app/static/css/tailwind.6f9d70cc.css | 1 - app/static/js/announce.js | 24 ++++++ app/static/js/app.js | 41 ++++++---- app/static/js/auth.js | 29 +++++-- app/static/js/cockpit-metrics.js | 8 +- app/static/js/cockpit.js | 8 +- app/static/js/dashboard.js | 4 +- app/static/js/forgot-password.js | 33 ++++++-- app/static/js/format-filter.js | 2 + app/static/js/login.js | 4 +- app/static/js/pdf-tools.js | 3 + app/static/js/pricing-segment.js | 4 +- app/static/js/redact.js | 11 ++- app/static/js/register.js | 3 +- app/static/js/reset-password.js | 3 +- app/templates/404.html | 2 +- app/templates/500.html | 2 +- app/templates/_components/button.html | 4 +- app/templates/_components/eyebrow.html | 2 +- app/templates/_components/text_input.html | 2 +- app/templates/_components/textarea.html | 2 +- app/templates/account_deleted.html | 2 +- app/templates/base.html | 56 ++++++++----- app/templates/cancel.html | 2 +- app/templates/cockpit.html | 24 +++--- app/templates/compress.html | 2 +- app/templates/contact.html | 6 +- app/templates/convert_pair.html | 4 +- app/templates/dashboard.html | 6 +- app/templates/enterprise.html | 50 ++++++------ app/templates/forgot-password.html | 5 +- app/templates/formats.html | 14 ++-- app/templates/impressum.html | 20 ++--- app/templates/index.html | 48 +++++------ app/templates/layouts/tool_page.html | 6 +- app/templates/login.html | 2 +- app/templates/partials/convert_tool.html | 37 +++++---- app/templates/partials/cookie_notice.html | 4 +- app/templates/partials/format_filter.html | 4 +- app/templates/partials/hosting_note.html | 2 +- app/templates/partials/pdf_tool.html | 32 ++++---- app/templates/partials/redact_tool.html | 20 ++--- app/templates/pricing.html | 28 +++---- app/templates/privacy.html | 24 +++--- app/templates/register.html | 2 +- app/templates/reset-password.html | 6 +- app/templates/security.html | 6 +- app/templates/terms.html | 16 ++-- app/templates/tools.html | 12 +-- app/templates/verify-email.html | 4 +- changelog.d/2026-10-29-a11y-contrast.md | 29 +++++++ locale/de/LC_MESSAGES/messages.po | 3 + locale/en/LC_MESSAGES/messages.po | 3 + locale/messages.pot | 3 + scripts/make_testdata_a11y_preview.py | 64 +++++++++++++++ tailwind.config.js | 11 ++- tests/test_accessibility.py | 67 +++++++++++++++ tests/test_cancel_page.py | 10 ++- tests/test_components.py | 8 +- tests/test_homepage_outline.py | 4 +- tests/test_public_claims.py | 99 +++++++++++++++++++++-- tests/test_template_scripts.py | 7 +- 63 files changed, 661 insertions(+), 284 deletions(-) create mode 100644 app/static/css/tailwind.3e9d4c71.css delete mode 100644 app/static/css/tailwind.6f9d70cc.css create mode 100644 app/static/js/announce.js create mode 100644 changelog.d/2026-10-29-a11y-contrast.md create mode 100644 scripts/make_testdata_a11y_preview.py create mode 100644 tests/test_accessibility.py diff --git a/app/static/css/tailwind.3e9d4c71.css b/app/static/css/tailwind.3e9d4c71.css new file mode 100644 index 00000000..cb6facdd --- /dev/null +++ b/app/static/css/tailwind.3e9d4c71.css @@ -0,0 +1 @@ +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.\!visible{visibility:visible!important}.visible{visibility:visible}.\!invisible{visibility:hidden!important}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-x-0{left:0;right:0}.-top-3{top:-.75rem}.bottom-0{bottom:0}.left-1\/2{left:50%}.right-0{right:0}.top-10{top:2.5rem}.z-40{z-index:40}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-5{margin-left:1.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.h-px{height:1px}.max-h-32{max-height:8rem}.max-h-48{max-height:12rem}.min-h-11{min-height:2.75rem}.min-h-5{min-height:1.25rem}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-md{max-width:28rem}.max-w-page{max-width:64rem}.max-w-prose-narrow{max-width:42rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.scroll-mt-20{scroll-margin-top:5rem}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-1{row-gap:.25rem}.gap-y-1\.5{row-gap:.375rem}.gap-y-2{row-gap:.5rem}.gap-y-4{row-gap:1rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.125rem*var(--tw-space-y-reverse));margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.375rem*var(--tw-space-y-reverse));margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2.5rem*var(--tw-space-y-reverse));margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-2\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.625rem*var(--tw-space-y-reverse));margin-top:calc(.625rem*(1 - var(--tw-space-y-reverse)))}.space-y-20>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(5rem*var(--tw-space-y-reverse));margin-top:calc(5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-y-section-y>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(5rem*var(--tw-space-y-reverse));margin-top:calc(5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-800>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(31 41 55/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl,.rounded-card{border-radius:1rem}.rounded-control{border-radius:.625rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-r-lg{border-bottom-right-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-900\/50{border-color:rgba(120,53,15,.5)}.border-brand{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-brand\/30{border-color:rgba(99,102,241,.3)}.border-emerald-700{--tw-border-opacity:1;border-color:rgb(4 120 87/var(--tw-border-opacity,1))}.border-emerald-800{--tw-border-opacity:1;border-color:rgb(6 95 70/var(--tw-border-opacity,1))}.border-emerald-900\/50{border-color:rgba(6,78,59,.5)}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity,1))}.border-gray-800\/60{border-color:rgba(31,41,55,.6)}.border-hairline{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity,1))}.border-hairline\/50{border-color:rgba(31,41,55,.5)}.border-red-800{--tw-border-opacity:1;border-color:rgb(153 27 27/var(--tw-border-opacity,1))}.border-red-900{--tw-border-opacity:1;border-color:rgb(127 29 29/var(--tw-border-opacity,1))}.border-sky-900\/50{border-color:rgba(12,74,110,.5)}.border-sky-900\/60{border-color:rgba(12,74,110,.6)}.bg-amber-950{--tw-bg-opacity:1;background-color:rgb(69 26 3/var(--tw-bg-opacity,1))}.bg-amber-950\/40{background-color:rgba(69,26,3,.4)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-brand-dark{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-brand\/10{background-color:rgba(99,102,241,.1)}.bg-brand\/20{background-color:rgba(99,102,241,.2)}.bg-brand\/5{background-color:rgba(99,102,241,.05)}.bg-emerald-700{--tw-bg-opacity:1;background-color:rgb(4 120 87/var(--tw-bg-opacity,1))}.bg-emerald-950{--tw-bg-opacity:1;background-color:rgb(2 44 34/var(--tw-bg-opacity,1))}.bg-emerald-950\/40{background-color:rgba(2,44,34,.4)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/40{background-color:rgba(31,41,55,.4)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/50{background-color:rgba(17,24,39,.5)}.bg-gray-950{--tw-bg-opacity:1;background-color:rgb(3 7 18/var(--tw-bg-opacity,1))}.bg-red-950{--tw-bg-opacity:1;background-color:rgb(69 10 10/var(--tw-bg-opacity,1))}.bg-red-950\/40{background-color:rgba(69,10,10,.4)}.bg-sky-950\/40{background-color:rgba(8,47,73,.4)}.bg-sky-950\/50{background-color:rgba(8,47,73,.5)}.bg-surface-raised{--tw-bg-opacity:1;background-color:rgb(23 25 34/var(--tw-bg-opacity,1))}.bg-surface-sunken{--tw-bg-opacity:1;background-color:rgb(10 12 16/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8,.p-card-pad-lg{padding:2rem}.p-card-pad-md{padding:1.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-16{padding-bottom:4rem;padding-top:4rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-2\.5{padding-bottom:.625rem;padding-top:.625rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-3\.5{padding-bottom:.875rem;padding-top:.875rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-8xl{font-size:6rem;line-height:1}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-eyebrow{font-size:.75rem;font-weight:600;letter-spacing:.08em;line-height:1rem}.text-h-page{font-size:1.875rem;font-weight:700;line-height:2.25rem}.text-h-sect{font-size:1.5rem;font-weight:700;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-brand{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.text-brand-light{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-ink{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-ink-dim{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-ink-faint{--tw-text-opacity:1;color:rgb(139 146 158/var(--tw-text-opacity,1))}.text-ink-muted{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-sky-200{--tw-text-opacity:1;color:rgb(186 230 253/var(--tw-text-opacity,1))}.text-sky-300{--tw-text-opacity:1;color:rgb(125 211 252/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.accent-brand{accent-color:#6366f1}.\!opacity-0{opacity:0!important}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-card{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-card{--tw-shadow:0 1px 0 hsla(0,0%,100%,.03),0 8px 32px rgba(0,0,0,.45);--tw-shadow-colored:0 1px 0 var(--tw-shadow-color),0 8px 32px var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.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)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.\[color-scheme\:dark\]{color-scheme:dark}.placeholder\:text-ink-faint::-moz-placeholder{--tw-text-opacity:1;color:rgb(139 146 158/var(--tw-text-opacity,1))}.placeholder\:text-ink-faint::placeholder{--tw-text-opacity:1;color:rgb(139 146 158/var(--tw-text-opacity,1))}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:0}.after\:rounded-card:after{border-radius:1rem;content:var(--tw-content)}.hover\:border-brand:hover{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.hover\:border-red-500:hover{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity,1))}.hover\:border-red-700:hover{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.hover\:bg-brand-deep:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.hover\:bg-emerald-800:hover{--tw-bg-opacity:1;background-color:rgb(6 95 70/var(--tw-bg-opacity,1))}.hover\:bg-emerald-900:hover{--tw-bg-opacity:1;background-color:rgb(6 78 59/var(--tw-bg-opacity,1))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.hover\:bg-gray-800:hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.hover\:bg-gray-800\/50:hover{background-color:rgba(31,41,55,.5)}.hover\:text-amber-300:hover{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.hover\:text-brand-light:hover{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-ink:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-sky-200:hover{--tw-text-opacity:1;color:rgb(186 230 253/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:not-sr-only:focus{height:auto;margin:0;overflow:visible;padding:0;position:static;width:auto;clip:auto;white-space:normal}.focus\:fixed:focus{position:fixed}.focus\:left-4:focus{left:1rem}.focus\:top-4:focus{top:1rem}.focus\:z-50:focus{z-index:50}.focus\:rounded-control:focus{border-radius:.625rem}.focus\:border-brand:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:bg-brand-dark:focus{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.focus\:px-4:focus{padding-left:1rem;padding-right:1rem}.focus\:py-2:focus{padding-bottom:.5rem;padding-top:.5rem}.focus\:text-white:focus{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-brand:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-brand:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus-visible\:after\:ring-2:focus-visible:after{content:var(--tw-content);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:after\:ring-brand:focus-visible:after{content:var(--tw-content);--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:focus-within .group-focus-within\:visible{visibility:visible}.group:focus-within .group-focus-within\:opacity-100{opacity:1}.group:hover .group-hover\:visible{visibility:visible}.group:hover .group-hover\:text-brand-light{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xs{font-size:.75rem;line-height:1rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}}@media (min-width:1024px){.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media print{.print\:hidden{display:none}.print\:bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.print\:text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.print\:text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}} \ No newline at end of file diff --git a/app/static/css/tailwind.6f9d70cc.css b/app/static/css/tailwind.6f9d70cc.css deleted file mode 100644 index 65616d2d..00000000 --- a/app/static/css/tailwind.6f9d70cc.css +++ /dev/null @@ -1 +0,0 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.\!visible{visibility:visible!important}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-x-0{left:0;right:0}.-top-3{top:-.75rem}.bottom-0{bottom:0}.left-1\/2{left:50%}.right-0{right:0}.top-10{top:2.5rem}.z-40{z-index:40}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-5{margin-left:1.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.h-px{height:1px}.max-h-32{max-height:8rem}.max-h-48{max-height:12rem}.min-h-11{min-height:2.75rem}.min-h-5{min-height:1.25rem}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-md{max-width:28rem}.max-w-page{max-width:64rem}.max-w-prose-narrow{max-width:42rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.scroll-mt-20{scroll-margin-top:5rem}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-1{row-gap:.25rem}.gap-y-1\.5{row-gap:.375rem}.gap-y-2{row-gap:.5rem}.gap-y-4{row-gap:1rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.125rem*var(--tw-space-y-reverse));margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.375rem*var(--tw-space-y-reverse));margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2.5rem*var(--tw-space-y-reverse));margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-2\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.625rem*var(--tw-space-y-reverse));margin-top:calc(.625rem*(1 - var(--tw-space-y-reverse)))}.space-y-20>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(5rem*var(--tw-space-y-reverse));margin-top:calc(5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-y-section-y>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(5rem*var(--tw-space-y-reverse));margin-top:calc(5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-800>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(31 41 55/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl,.rounded-card{border-radius:1rem}.rounded-control{border-radius:.625rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-r-lg{border-bottom-right-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-900\/50{border-color:rgba(120,53,15,.5)}.border-brand{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-brand\/30{border-color:rgba(99,102,241,.3)}.border-emerald-700{--tw-border-opacity:1;border-color:rgb(4 120 87/var(--tw-border-opacity,1))}.border-emerald-800{--tw-border-opacity:1;border-color:rgb(6 95 70/var(--tw-border-opacity,1))}.border-emerald-900\/50{border-color:rgba(6,78,59,.5)}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity,1))}.border-gray-800\/60{border-color:rgba(31,41,55,.6)}.border-hairline{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity,1))}.border-hairline\/50{border-color:rgba(31,41,55,.5)}.border-red-800{--tw-border-opacity:1;border-color:rgb(153 27 27/var(--tw-border-opacity,1))}.border-red-900{--tw-border-opacity:1;border-color:rgb(127 29 29/var(--tw-border-opacity,1))}.border-sky-900\/50{border-color:rgba(12,74,110,.5)}.border-sky-900\/60{border-color:rgba(12,74,110,.6)}.bg-amber-950{--tw-bg-opacity:1;background-color:rgb(69 26 3/var(--tw-bg-opacity,1))}.bg-amber-950\/40{background-color:rgba(69,26,3,.4)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-brand{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.bg-brand\/10{background-color:rgba(99,102,241,.1)}.bg-brand\/20{background-color:rgba(99,102,241,.2)}.bg-brand\/5{background-color:rgba(99,102,241,.05)}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-emerald-950{--tw-bg-opacity:1;background-color:rgb(2 44 34/var(--tw-bg-opacity,1))}.bg-emerald-950\/40{background-color:rgba(2,44,34,.4)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/40{background-color:rgba(31,41,55,.4)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/50{background-color:rgba(17,24,39,.5)}.bg-gray-950{--tw-bg-opacity:1;background-color:rgb(3 7 18/var(--tw-bg-opacity,1))}.bg-red-950{--tw-bg-opacity:1;background-color:rgb(69 10 10/var(--tw-bg-opacity,1))}.bg-red-950\/40{background-color:rgba(69,10,10,.4)}.bg-sky-950\/40{background-color:rgba(8,47,73,.4)}.bg-sky-950\/50{background-color:rgba(8,47,73,.5)}.bg-surface-raised{--tw-bg-opacity:1;background-color:rgb(23 25 34/var(--tw-bg-opacity,1))}.bg-surface-sunken{--tw-bg-opacity:1;background-color:rgb(10 12 16/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8,.p-card-pad-lg{padding:2rem}.p-card-pad-md{padding:1.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-16{padding-bottom:4rem;padding-top:4rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-2\.5{padding-bottom:.625rem;padding-top:.625rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-3\.5{padding-bottom:.875rem;padding-top:.875rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-8xl{font-size:6rem;line-height:1}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-eyebrow{font-size:.75rem;font-weight:600;letter-spacing:.08em;line-height:1rem}.text-h-page{font-size:1.875rem;font-weight:700;line-height:2.25rem}.text-h-sect{font-size:1.5rem;font-weight:700;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-brand{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.text-brand-light{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-ink{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-ink-faint{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-ink-muted{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-sky-200{--tw-text-opacity:1;color:rgb(186 230 253/var(--tw-text-opacity,1))}.text-sky-300{--tw-text-opacity:1;color:rgb(125 211 252/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.accent-brand{accent-color:#6366f1}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-card{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-card{--tw-shadow:0 1px 0 hsla(0,0%,100%,.03),0 8px 32px rgba(0,0,0,.45);--tw-shadow-colored:0 1px 0 var(--tw-shadow-color),0 8px 32px var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.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)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.\[color-scheme\:dark\]{color-scheme:dark}.placeholder\:text-ink-faint::-moz-placeholder{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.placeholder\:text-ink-faint::placeholder{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:0}.after\:rounded-card:after{border-radius:1rem;content:var(--tw-content)}.hover\:border-brand:hover{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.hover\:border-red-500:hover{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity,1))}.hover\:border-red-700:hover{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.hover\:bg-brand-dark:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.hover\:bg-emerald-500:hover{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.hover\:bg-emerald-900:hover{--tw-bg-opacity:1;background-color:rgb(6 78 59/var(--tw-bg-opacity,1))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.hover\:bg-gray-800:hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.hover\:bg-gray-800\/50:hover{background-color:rgba(31,41,55,.5)}.hover\:text-amber-300:hover{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.hover\:text-brand:hover{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-ink:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-sky-200:hover{--tw-text-opacity:1;color:rgb(186 230 253/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-brand:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-brand:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-brand:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus-visible\:after\:ring-2:focus-visible:after{content:var(--tw-content);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:after\:ring-brand:focus-visible:after{content:var(--tw-content);--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:visible{visibility:visible}.group:hover .group-hover\:text-brand{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}}@media (min-width:1024px){.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media print{.print\:hidden{display:none}.print\:bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.print\:text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.print\:text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}} \ No newline at end of file diff --git a/app/static/js/announce.js b/app/static/js/announce.js new file mode 100644 index 00000000..50613a53 --- /dev/null +++ b/app/static/js/announce.js @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// FM_announce(text) reads a short status of the tools ("Processing…", "Your +// file is ready…") to screen-reader users through the page's polite live +// region (#fm-live in base.html). Only text that is already on the screen is +// passed in. The text is set after a short pause, so the same message twice is +// announced twice, and it is cleared after a few seconds, so no stale "done" +// is left for the next run. FM_announce('') empties the region at once (the +// error paths call it, so a pending "Processing…" can't follow the error). +(function () { + let setTimer = null; + let clearTimer = null; + window.FM_announce = function (text) { + const region = document.getElementById('fm-live'); + if (!region) return; + clearTimeout(setTimer); + clearTimeout(clearTimer); + region.textContent = ''; + if (!text) return; + setTimer = setTimeout(function () { + region.textContent = text.trim(); + clearTimer = setTimeout(function () { region.textContent = ''; }, 8000); + }, 50); + }; +})(); diff --git a/app/static/js/app.js b/app/static/js/app.js index d9ef0b6c..5d93172b 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -145,15 +145,15 @@ function setMode(mode) { btnCompress.setAttribute('aria-pressed', mode === 'compress' ? 'true' : 'false'); if (mode === 'convert') { - btnConvert.classList.add('bg-brand', 'text-white'); + btnConvert.classList.add('bg-brand-dark', 'text-white'); btnConvert.classList.remove('text-gray-400'); - btnCompress.classList.remove('bg-brand', 'text-white'); + btnCompress.classList.remove('bg-brand-dark', 'text-white'); btnCompress.classList.add('text-gray-400'); convertBtn.textContent = (window.FM_I18N && window.FM_I18N.convert) || 'Convert'; } else { - btnCompress.classList.add('bg-brand', 'text-white'); + btnCompress.classList.add('bg-brand-dark', 'text-white'); btnCompress.classList.remove('text-gray-400'); - btnConvert.classList.remove('bg-brand', 'text-white'); + btnConvert.classList.remove('bg-brand-dark', 'text-white'); btnConvert.classList.add('text-gray-400'); convertBtn.textContent = (window.FM_I18N && window.FM_I18N.compress) || 'Compress'; } @@ -200,12 +200,14 @@ function syncCompressModeButtons() { const tBtn = document.getElementById('cmode-target'); if (!qBtn || !tBtn) return; const isQuality = compressMode === 'quality'; - qBtn.classList.toggle('bg-brand', isQuality); - qBtn.classList.toggle('text-white', isQuality); - qBtn.classList.toggle('text-gray-200', !isQuality); - tBtn.classList.toggle('bg-brand', !isQuality); - tBtn.classList.toggle('text-white', !isQuality); - tBtn.classList.toggle('text-gray-200', isQuality); + for (const [btn, on] of [[qBtn, isQuality], [tBtn, !isQuality]]) { + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + btn.classList.toggle('bg-brand-dark', on); + btn.classList.toggle('text-white', on); + btn.classList.toggle('bg-gray-800', !on); + btn.classList.toggle('hover:bg-gray-700', !on); + btn.classList.toggle('text-gray-200', !on); + } } function updateTargetSizeLabel() { @@ -286,7 +288,7 @@ function renderFileList() { name.textContent = file.name; const size = document.createElement('span'); - size.className = 'text-xs text-gray-500 shrink-0'; + size.className = 'text-xs text-ink-faint shrink-0'; size.textContent = formatBytes(file.size); // Batch convert: each file picks its own target — mixed-format @@ -295,7 +297,7 @@ function renderFileList() { if (isBatch && currentMode === 'convert') { perRowSelect = document.createElement('select'); perRowSelect.className = - 'per-row-target bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs shrink-0 focus:outline-none focus:border-brand transition-colors'; + 'per-row-target bg-gray-800 border border-gray-700 rounded px-2 py-1 text-base sm:text-xs shrink-0 focus:outline-none focus:ring-2 focus:ring-brand focus:border-brand transition-colors'; perRowSelect.setAttribute('aria-label', i18nText('ariaTargetFormatFor', 'Target format for {name}', { name: file.name })); const ext = getExtension(file.name); const targets = supportedConversions[ext] || []; @@ -335,7 +337,7 @@ function renderFileList() { const rm = document.createElement('button'); rm.type = 'button'; - rm.className = 'text-xs text-gray-600 hover:text-red-400 transition-colors shrink-0'; + rm.className = 'text-xs text-ink-faint hover:text-red-400 transition-colors shrink-0'; rm.setAttribute('aria-label', i18nText('ariaRemoveFile', 'Remove {name}', { name: file.name })); rm.textContent = '✕'; rm.addEventListener('click', e => { @@ -1164,7 +1166,7 @@ async function submitForm() { label.textContent = i18nText('downloadResultSize', 'Download Result ({size} MB)', { size: mb }); } else { label.textContent = isBatch - ? i18nText('downloadZip', 'Download ZIP ({count} files)', { count: selectedFiles.length }) + ? i18nText('downloadZip', 'Download ZIP ({count} files)', { count: zipCount(res) }) : i18nText('downloadResult', 'Download Result'); } } @@ -1177,6 +1179,12 @@ async function submitForm() { // ── State helpers ───────────────────────────────────────────────────────────── +// Files in the batch ZIP: the succeeded ones, not all that were selected. +function zipCount(res) { + const ok = parseInt(res.headers.get('X-FileMorph-Batch-Succeeded'), 10); + return Number.isNaN(ok) ? selectedFiles.length : ok; +} + function showProgress(text) { document.getElementById('progress-text').textContent = text; document.getElementById('progress-section').classList.remove('hidden'); @@ -1188,6 +1196,7 @@ function showProgress(text) { hideBatchSummary(); setConvertErrorUpsell(false); document.getElementById('convert-btn').disabled = true; + window.FM_announce && window.FM_announce(text); } function showResult() { @@ -1196,9 +1205,12 @@ function showResult() { document.getElementById('error-section').classList.add('hidden'); document.getElementById('convert-btn').disabled = false; setConvertErrorUpsell(false); + // Read out what the result card shows: download label, hints, batch summary. + window.FM_announce && window.FM_announce(document.getElementById('result-section').innerText); } function showError(msg, code) { + window.FM_announce && window.FM_announce(''); document.getElementById('error-text').textContent = msg; document.getElementById('progress-section').classList.add('hidden'); document.getElementById('result-section').classList.add('hidden'); @@ -1208,6 +1220,7 @@ function showError(msg, code) { } function resetResultState() { + window.FM_announce && window.FM_announce(''); document.getElementById('progress-section').classList.add('hidden'); document.getElementById('result-section').classList.add('hidden'); document.getElementById('error-section').classList.add('hidden'); diff --git a/app/static/js/auth.js b/app/static/js/auth.js index 74f4cbb6..4e952ff1 100644 --- a/app/static/js/auth.js +++ b/app/static/js/auth.js @@ -88,21 +88,40 @@ const initial = user.email[0].toUpperCase(); desktop.innerHTML = `
- - `; desktop.querySelector('[data-action="signout"]').addEventListener('click', () => window.FM.logout()); + // The panel opens on hover and on keyboard focus (CSS). Escape hides it with + // focus back on the button (WCAG 1.4.13); leaving the menu resets it. + const group = desktop.querySelector('.group'); + const avatar = group.querySelector('button'); + const panel = avatar.nextElementSibling; + const setOpen = (open) => { + panel.classList.toggle('!invisible', !open); + panel.classList.toggle('!opacity-0', !open); + avatar.setAttribute('aria-expanded', open ? 'true' : 'false'); + }; + group.addEventListener('focusin', () => avatar.setAttribute('aria-expanded', panel.classList.contains('!invisible') ? 'false' : 'true')); + avatar.addEventListener('click', () => setOpen(true)); // reopens after Escape + group.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { setOpen(false); avatar.focus(); } + }); + group.addEventListener('focusout', (e) => { + if (!group.contains(e.relatedTarget)) { panel.classList.remove('!invisible', '!opacity-0'); avatar.setAttribute('aria-expanded', 'false'); } + }); + group.addEventListener('mouseleave', () => panel.classList.remove('!invisible', '!opacity-0')); if (mobile) { mobile.innerHTML = ` - ${escapeHtml(lDashboard)} - `; + ${escapeHtml(lDashboard)} + `; mobile.querySelector('[data-action="signout-mobile"]').addEventListener('click', () => window.FM.logout()); } } else { diff --git a/app/static/js/cockpit-metrics.js b/app/static/js/cockpit-metrics.js index a92f4598..ef8c1448 100644 --- a/app/static/js/cockpit-metrics.js +++ b/app/static/js/cockpit-metrics.js @@ -61,10 +61,10 @@ function card(label, value, series, color) { return `
-

${escapeHtml(label)}

+

${escapeHtml(label)}

${value.toLocaleString()}

${sparkline(series, color === 'text-brand' ? '#6366f1' : color === 'text-emerald-400' ? '#34d399' : '#9ca3af')}
-

last ${state.days} d

+

last ${state.days} d

`; } @@ -72,7 +72,7 @@ if (!pairs || pairs.length === 0) { const label = (window.FM_I18N && window.FM_I18N.noConversionsYet) || 'No conversions yet.'; const empty = document.createElement('li'); - empty.className = 'text-xs text-gray-500'; + empty.className = 'text-xs text-ink-faint'; empty.textContent = label; $('metrics-pairs').replaceChildren(empty); return; @@ -94,7 +94,7 @@ const hint = $('metrics-failure-hint'); if (rate === null || rate === undefined) { el.textContent = '—'; - el.className = 'text-3xl font-bold mt-1 text-gray-500'; + el.className = 'text-3xl font-bold mt-1 text-ink-faint'; // Distinguish "no traffic" from "too little traffic to be meaningful". // The endpoint returns null + a sample_size for both, so we read the // count to pick the right user-facing copy. diff --git a/app/static/js/cockpit.js b/app/static/js/cockpit.js index 9620595d..73e65889 100644 --- a/app/static/js/cockpit.js +++ b/app/static/js/cockpit.js @@ -5,7 +5,7 @@ const TIER_COLOR = { free: 'text-gray-400', - pro: 'text-brand', + pro: 'text-brand-light', business: 'text-emerald-400', enterprise: 'text-yellow-400', }; @@ -58,7 +58,7 @@ .map( (c) => `
-

${escapeHtml(c.label)}

+

${escapeHtml(c.label)}

${c.value.toLocaleString()}

`, ) @@ -135,7 +135,7 @@ $('user-count').textContent = `(${state.total.toLocaleString()})`; const tbody = $('users-tbody'); if (!items.length) { - tbody.innerHTML = 'No users match these filters.'; + tbody.innerHTML = 'No users match these filters.'; return; } tbody.innerHTML = items @@ -147,7 +147,7 @@ ${escapeHtml(u.tier)} ${escapeHtml(u.role)} ${u.is_active ? '✓' : '✗'} - ${u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '—'} + ${u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '—'} diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index 8e020372..13d6672d 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -105,7 +105,7 @@ async function loadKeys() { const list = document.getElementById('keys-list'); if (keys.length === 0) { const empty = document.createElement('p'); - empty.className = 'text-sm text-gray-500'; + empty.className = 'text-sm text-ink-faint'; empty.textContent = _t('noApiKeys', 'No API keys yet. Create one above.'); list.replaceChildren(empty); return; @@ -119,7 +119,7 @@ async function loadKeys() { return '
' + '
' + '

' + escapeHtml(k.label) + '

' + - '

' + escapeHtml(labelCreated) + ' ' + new Date(k.created_at).toLocaleDateString(dateLocale) + + '

' + escapeHtml(labelCreated) + ' ' + new Date(k.created_at).toLocaleDateString(dateLocale) + (k.last_used_at ? ' \u00b7 ' + escapeHtml(labelLastUsed) + ' ' + new Date(k.last_used_at).toLocaleDateString(dateLocale) : ' \u00b7 ' + escapeHtml(labelNeverUsed)) + '

' + '
' + '' + diff --git a/app/static/js/forgot-password.js b/app/static/js/forgot-password.js index 18aca4d5..58fa323f 100644 --- a/app/static/js/forgot-password.js +++ b/app/static/js/forgot-password.js @@ -1,19 +1,38 @@ // SPDX-License-Identifier: AGPL-3.0-or-later document.addEventListener('DOMContentLoaded', function () { document.getElementById('send-btn').addEventListener('click', doReset); - document.addEventListener('keydown', function (e) { if (e.key === 'Enter') doReset(); }); + // Enter in the field sends — not Enter on a link or button (the skip link is + // the first Tab stop and would send an empty form). + document.addEventListener('keydown', function (e) { if (e.key === 'Enter' && e.target.matches('input')) doReset(); }); }); async function doReset() { const btn = document.getElementById('send-btn'); - const email = document.getElementById('email').value.trim(); + const field = document.getElementById('email'); + const err = document.getElementById('error-msg'); + // Empty or malformed: the browser says so at the field (required, type=email). + if (!field.reportValidity()) return; + const label = btn.textContent; + err.classList.add('hidden'); btn.disabled = true; btn.textContent = (window.FM_I18N && window.FM_I18N.sending) || 'Sending\u2026'; - await fetch('/api/v1/auth/forgot-password', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }), - }); + let ok = false; + try { + const res = await fetch('/api/v1/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: field.value.trim() }), + }); + ok = res.ok; + } catch (_) { /* network error: reported below */ } + if (!ok) { + // No "email sent" unless the server took the request. + err.textContent = (window.FM_I18N && window.FM_I18N.pdfToolError) || 'Something went wrong. Please try again.'; + err.classList.remove('hidden'); + btn.disabled = false; + btn.textContent = label; + return; + } document.getElementById('form-section').classList.add('hidden'); document.getElementById('success-msg').classList.remove('hidden'); } diff --git a/app/static/js/format-filter.js b/app/static/js/format-filter.js index 10ba4f78..c68ddbf4 100644 --- a/app/static/js/format-filter.js +++ b/app/static/js/format-filter.js @@ -217,6 +217,8 @@ input.focus(); }); window.addEventListener('hashchange', () => { + // A jump to an anchor (the skip link's #main-content) is not a search. + if (location.hash && !new URLSearchParams(location.hash.slice(1)).has('q')) return; const q = readHash(); if (q !== input.value) { input.value = q; diff --git a/app/static/js/login.js b/app/static/js/login.js index b9acda6c..ac3e6f17 100644 --- a/app/static/js/login.js +++ b/app/static/js/login.js @@ -32,5 +32,7 @@ async function doLogin() { document.addEventListener('DOMContentLoaded', function () { document.getElementById('login-btn').addEventListener('click', doLogin); - document.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); }); + // Enter in a field signs in — not Enter on a link or button (the skip link is + // the first Tab stop and would send an empty form). + document.addEventListener('keydown', function (e) { if (e.key === 'Enter' && e.target.matches('input')) doLogin(); }); }); diff --git a/app/static/js/pdf-tools.js b/app/static/js/pdf-tools.js index 66471fe8..5f65d8dd 100644 --- a/app/static/js/pdf-tools.js +++ b/app/static/js/pdf-tools.js @@ -81,6 +81,7 @@ }; function showError(msg, code, field) { + window.FM_announce && window.FM_announce(''); hide($('pdf-progress')); // An error replaces a result still on screen from an earlier run. hide($('pdf-result')); @@ -472,6 +473,7 @@ setExtractLinkVisible(false); hide($('pdf-result')); show($('pdf-progress')); + window.FM_announce && window.FM_announce($('pdf-progress-text').textContent); const fd = new FormData(); fd.append('file', selectedFile); @@ -516,6 +518,7 @@ } show($('pdf-result')); + window.FM_announce && window.FM_announce($('pdf-result').innerText); } catch (_) { showError(t('pdfToolError', 'Something went wrong. Please try again.')); } finally { diff --git a/app/static/js/pricing-segment.js b/app/static/js/pricing-segment.js index b4cea889..27b7b64a 100644 --- a/app/static/js/pricing-segment.js +++ b/app/static/js/pricing-segment.js @@ -18,10 +18,10 @@ document.addEventListener("DOMContentLoaded", () => { panelGov.classList.toggle("hidden", dev); segDev.setAttribute("aria-pressed", dev ? "true" : "false"); segGov.setAttribute("aria-pressed", dev ? "false" : "true"); - segDev.classList.toggle("bg-brand", dev); + segDev.classList.toggle("bg-brand-dark", dev); segDev.classList.toggle("text-white", dev); segDev.classList.toggle("text-gray-400", !dev); - segGov.classList.toggle("bg-brand", !dev); + segGov.classList.toggle("bg-brand-dark", !dev); segGov.classList.toggle("text-white", !dev); segGov.classList.toggle("text-gray-400", dev); } diff --git a/app/static/js/redact.js b/app/static/js/redact.js index 144ab6e4..db0342f0 100644 --- a/app/static/js/redact.js +++ b/app/static/js/redact.js @@ -50,6 +50,7 @@ } function showError(msg) { + window.FM_announce && window.FM_announce(''); hide($('redact-progress')); $('redact-error-text').textContent = msg; show($('redact-error')); @@ -109,7 +110,7 @@ group.querySelectorAll('button[data-mode]').forEach((b) => { const on = b === btn; b.setAttribute('aria-pressed', on ? 'true' : 'false'); - b.classList.toggle('bg-brand', on); + b.classList.toggle('bg-brand-dark', on); b.classList.toggle('text-white', on); b.classList.toggle('bg-gray-800', !on); b.classList.toggle('text-gray-200', !on); @@ -125,6 +126,7 @@ hide($('redact-findings')); $('redact-progress-text').textContent = t('redactScanning', 'Scanning…'); show($('redact-progress')); + window.FM_announce && window.FM_announce($('redact-progress-text').textContent); const fd = new FormData(); fd.append('file', selectedFile); @@ -149,6 +151,7 @@ $('redact-findings-count').textContent = findings.length ? t('redactFindings', '{count} item(s) found', { count: findings.length }) : t('redactNoFindings', 'No PII detected in this file.'); + window.FM_announce && window.FM_announce($('redact-findings-count').textContent); let hasPhone = false; findings.forEach((f) => { @@ -163,12 +166,12 @@ typeSpan.className = 'text-gray-200'; typeSpan.textContent = entityLabel(f.entity_type); const valSpan = document.createElement('span'); - valSpan.className = 'text-gray-500 ml-1'; + valSpan.className = 'text-ink-faint ml-1'; valSpan.textContent = maskValue(f.value); left.appendChild(typeSpan); left.appendChild(valSpan); const right = document.createElement('span'); - right.className = conf < 90 ? 'text-amber-400' : 'text-gray-600'; + right.className = conf < 90 ? 'text-amber-400' : 'text-ink-faint'; right.textContent = `${f.location} · ${conf}%`; row.appendChild(left); row.appendChild(right); @@ -218,6 +221,7 @@ hide($('redact-error')); $('redact-progress-text').textContent = t('redactApplying', 'Redacting…'); show($('redact-progress')); + window.FM_announce && window.FM_announce($('redact-progress-text').textContent); const fd = new FormData(); fd.append('file', selectedFile); @@ -245,6 +249,7 @@ $('redact-result-summary').textContent = summary; hide($('redact-findings')); show($('redact-result')); + window.FM_announce && window.FM_announce($('redact-result').innerText); } catch (_) { showError(t('redactError', 'Redaction failed. Please try again.')); } diff --git a/app/static/js/register.js b/app/static/js/register.js index 88628384..b15dae8d 100644 --- a/app/static/js/register.js +++ b/app/static/js/register.js @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later document.addEventListener('DOMContentLoaded', function () { document.getElementById('reg-btn').addEventListener('click', doRegister); - document.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); }); + // Enter in a field registers — not Enter on a link or button (skip link). + document.addEventListener('keydown', function (e) { if (e.key === 'Enter' && e.target.matches('input')) doRegister(); }); }); function _t(key, fallback) { diff --git a/app/static/js/reset-password.js b/app/static/js/reset-password.js index efc4d9e3..46717eac 100644 --- a/app/static/js/reset-password.js +++ b/app/static/js/reset-password.js @@ -70,7 +70,8 @@ } document.getElementById('reset-btn').addEventListener('click', function () { doReset(token); }); document.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { doReset(token); } + // Only from a field — Enter on a link or button (skip link) is not a submit. + if (e.key === 'Enter' && e.target.matches('input')) { doReset(token); } }); }); })(); diff --git a/app/templates/404.html b/app/templates/404.html index 4ad66581..4a75932b 100644 --- a/app/templates/404.html +++ b/app/templates/404.html @@ -3,7 +3,7 @@ {% block title %}{{ _('404 – Not Found') }} | FileMorph{% endblock %} {% block content %}
-

404

+

404

{{ _('Page not found') }}

{{ _('The page you are looking for does not exist or has been moved.') }} diff --git a/app/templates/500.html b/app/templates/500.html index 64675669..c71bb1a0 100644 --- a/app/templates/500.html +++ b/app/templates/500.html @@ -3,7 +3,7 @@ {% block title %}{{ _('500 – Server Error') }} | FileMorph{% endblock %} {% block content %}

-

500

+

500

{{ _('Internal server error') }}

{{ _('Something went wrong on our end. Please try again in a moment.') }} diff --git a/app/templates/_components/button.html b/app/templates/_components/button.html index 137d2178..3c713d7f 100644 --- a/app/templates/_components/button.html +++ b/app/templates/_components/button.html @@ -26,7 +26,7 @@ {%- else -%}{%- set size_cls = 'px-6 py-3 text-base' -%} {%- endif -%} {%- if disabled -%} - {%- set variant_cls = 'bg-surface-sunken text-ink-faint cursor-not-allowed' -%} + {%- set variant_cls = 'bg-surface-sunken text-ink-dim cursor-not-allowed' -%} {%- elif variant == 'secondary' -%} {%- set variant_cls = 'border border-hairline text-ink hover:border-brand hover:text-ink' -%} {%- elif variant == 'ghost' -%} @@ -34,7 +34,7 @@ {%- elif variant == 'danger' -%} {%- set variant_cls = 'border border-red-900 text-red-400' -%} {%- else -%} - {%- set variant_cls = 'bg-brand text-white hover:bg-brand-dark' -%} + {%- set variant_cls = 'bg-brand-dark text-white hover:bg-brand-deep' -%} {%- endif -%} {%- if full_width -%}{%- set width_cls = 'w-full' -%}{%- else -%}{%- set width_cls = '' -%}{%- endif -%} {%- if href and not disabled -%} diff --git a/app/templates/_components/eyebrow.html b/app/templates/_components/eyebrow.html index 628a5fba..61daf770 100644 --- a/app/templates/_components/eyebrow.html +++ b/app/templates/_components/eyebrow.html @@ -5,6 +5,6 @@ #} {% macro eyebrow(text, color='gray') %}

{{ text }}

{% endmacro %} diff --git a/app/templates/_components/text_input.html b/app/templates/_components/text_input.html index 961e8672..ce6f087a 100644 --- a/app/templates/_components/text_input.html +++ b/app/templates/_components/text_input.html @@ -21,7 +21,7 @@ {% if placeholder %}placeholder="{{ placeholder }}"{% endif %} {% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %} {% if required %}required{% endif %} - class="block w-full bg-surface-sunken border border-hairline rounded-control px-3 py-2.5 text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-brand focus:border-brand transition-colors" /> + class="block w-full bg-surface-sunken border border-hairline rounded-control px-3 py-2.5 text-base sm:text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-brand focus:border-brand transition-colors" /> {% if help %}

{{ help }}

{% endif %} diff --git a/app/templates/_components/textarea.html b/app/templates/_components/textarea.html index 9ab8bf27..164d52b5 100644 --- a/app/templates/_components/textarea.html +++ b/app/templates/_components/textarea.html @@ -22,7 +22,7 @@ {% if placeholder %}placeholder="{{ placeholder }}"{% endif %} {% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %} {% if required %}required{% endif %} - class="block w-full bg-surface-sunken border border-hairline rounded-control px-3 py-2.5 text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-brand focus:border-brand transition-colors resize-y"> + class="block w-full bg-surface-sunken border border-hairline rounded-control px-3 py-2.5 text-base sm:text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-brand focus:border-brand transition-colors resize-y"> {% if help %}

{{ help }}

{% endif %} diff --git a/app/templates/account_deleted.html b/app/templates/account_deleted.html index c7590094..7072bd8b 100644 --- a/app/templates/account_deleted.html +++ b/app/templates/account_deleted.html @@ -4,6 +4,6 @@

{{ _('Your account has been deleted') }}

{{ _('You\'ll receive a confirmation email shortly. You can re-register at any time — a new registration creates a fresh account with no link to this one.') }}

-

{{ _('← Back to FileMorph') }}

+

{{ _('← Back to FileMorph') }}

{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 70a9b74c..c08013a3 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -72,6 +72,11 @@ the dark body would print dark and swallow the receipt's black heading. On every other page that dark body is what keeps their light text legible when graphics are on (with them off those pages already print white paper), so it must stay there. #} + {# Skip link: the first Tab stop jumps past the navigation to #main-content. + Not on minimal_chrome pages (/cancel pins every link it shows). #} + {% if not minimal_chrome %} + {{ _('Skip to main content') }} + {% endif %}
{% if not minimal_chrome and accounts_enabled() %} {# Sign In only: registering is reachable from the login page, the @@ -147,13 +152,13 @@ hreflang="de" aria-label="{{ _('Switch to German') }}" {% if locale == 'de' %}aria-current="true"{% endif %} - class="text-xs uppercase tracking-wider {% if locale == 'de' %}text-white font-semibold{% else %}text-gray-500{% endif %}">DE + class="text-xs uppercase tracking-wider {% if locale == 'de' %}text-white font-semibold{% else %}text-ink-faint{% endif %}">DE EN + class="text-xs uppercase tracking-wider {% if locale == 'en' %}text-white font-semibold{% else %}text-ink-faint{% endif %}">EN
{% if accounts_enabled() %} {% endif %} + {# Skip-link target, right before each page's own
. #} +
+ {# The page's one polite live region: FM_announce() (announce.js) reads the + tools' "processing" / "done" messages to screen readers. Always present — + a region that is shown only when it changes is announced unreliably. #} +
{% block content %}{% endblock %} {% include "partials/cookie_notice.html" %}