/* Form modals on the native <dialog> element. Each dialog holds a <form method="dialog">: the browser's own
validation (required, type="email") must pass before the submit button closes it; Cancel and X use formnovalidate. */
.fm-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.fm-btn{--fm-accent:#0f766e;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 18px;border:1px solid #d4cdbf;border-radius:10px;
background:#fff;color:#0f172a;font-family:inherit;font-size:.9rem;font-weight:600;line-height:1.2;text-decoration:none;cursor:pointer;transition:background-color .15s,border-color .15s,box-shadow .15s}
.fm-btn:hover{background:#f7f4ee;border-color:#a8a090}
.fm-btn:focus-visible{outline:2px solid var(--fm-accent);outline-offset:2px}
.fm-btn.is-primary{background:var(--fm-accent);border-color:var(--fm-accent);color:#fff;box-shadow:0 6px 16px -8px rgba(15,118,110,.75)}
.fm-btn.is-primary:hover{background:#115e59;border-color:#115e59}
.fm-btn.is-block{width:100%}
.fm-btn svg{width:18px;height:18px;flex:none}
.fm{--fm-accent:#0f766e;--fm-accent-dark:#115e59;--fm-ink:#0f172a;--fm-muted:#475569;--fm-line:#e8e3d9;--fm-soft:#faf8f4;--fm-field:#848e9d;--fm-ring:rgba(15,118,110,.24);
--fm-radius:18px;--fm-w:440px;
width:min(var(--fm-w),calc(100% - 32px));max-width:none;max-height:calc(100dvh - 56px);margin:auto;padding:0;border:0;border-radius:var(--fm-radius);
background:#fff;color:var(--fm-ink);font-size:.95rem;line-height:1.55;overflow:hidden;
box-shadow:0 0 0 1px rgba(15,23,42,.06),0 28px 64px -18px rgba(15,23,42,.45)}
.fm[open]{display:flex;flex-direction:column;animation:fm-in .22s cubic-bezier(.2,.8,.2,1)}
.fm::backdrop{background:rgba(15,23,42,.55);backdrop-filter:blur(2px)}
.fm[open]::backdrop{animation:fm-fade .22s ease-out}
@keyframes fm-in{from{opacity:0;transform:translateY(10px) scale(.97)}}
@keyframes fm-fade{from{opacity:0}}
html:has(.fm:modal){overflow:hidden} /* stop the page scrolling behind an open modal */
.fm-form{display:flex;flex-direction:column;flex:1;min-height:0;margin:0}
.fm-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:22px 32px 26px}
.fm-title{margin:0;font-size:1.3rem;font-weight:700;letter-spacing:-.015em;line-height:1.3}
.fm-sub{margin:4px 0 0;color:var(--fm-muted);font-size:.9rem}
.fm-x{position:absolute;top:12px;right:12px;z-index:1;display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:10px;background:transparent;color:#475569;cursor:pointer;
transition:background-color .15s,color .15s}
.fm-x:hover{background:#f3efe7;color:var(--fm-ink)}
.fm-x:focus-visible{outline:2px solid var(--fm-accent);outline-offset:1px}
.fm-x svg{width:20px;height:20px}
/* Over a photo the X sits on a solid, slightly blurred chip so it keeps its contrast */
.fm-x.is-chip{border-radius:999px;background:rgba(255,255,255,.9);color:#0f172a;box-shadow:0 1px 3px rgba(15,23,42,.2);backdrop-filter:blur(6px)}
.fm-x.is-chip:hover{background:#fff}
.fm-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;padding:16px 24px;border-top:1px solid var(--fm-line);background:var(--fm-soft)}
/* Fields: 44px tall, 10px radius, the label always above the field */
.fm-field{display:flex;flex-direction:column;gap:6px;margin:0 0 16px}
.fm-label{color:var(--fm-ink);font-size:.85rem;font-weight:600;line-height:1.3}
.fm-opt{color:#5b6474;font-weight:400}
.fm-input{width:100%;height:44px;margin:0;padding:0 14px;border:1px solid var(--fm-field);border-radius:10px;background:#fff;color:var(--fm-ink);font-family:inherit;font-size:.95rem;
transition:border-color .15s,box-shadow .15s}
.fm-input::placeholder{color:#6b7280;opacity:1}
.fm-input:hover{border-color:#5f6b7b}
.fm-input:focus-visible{outline:none;border-color:var(--fm-accent);box-shadow:0 0 0 3px var(--fm-ring)}
.fm-input:user-invalid{border-color:#b91c1c}
.fm-input:user-invalid:focus-visible{box-shadow:0 0 0 3px rgba(185,28,28,.2)}
textarea.fm-input{display:block;height:auto;min-height:104px;padding:10px 14px;line-height:1.5;resize:vertical}
.fm-check{display:inline-flex;align-items:center;gap:8px;color:#334155;font-size:.875rem;cursor:pointer}
.fm-check input{appearance:none;flex:none;width:18px;height:18px;margin:0;border:1.5px solid var(--fm-field);border-radius:5px;background:#fff center/12px no-repeat;cursor:pointer;
transition:background-color .15s,border-color .15s}
.fm-check input:checked{border-color:var(--fm-accent);background-color:var(--fm-accent);
background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}
.fm-check input:focus-visible{outline:2px solid var(--fm-accent);outline-offset:2px}
.fm-link{border-radius:4px;color:var(--fm-accent);font-weight:600;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}
.fm-link:hover{color:var(--fm-accent-dark);text-decoration-thickness:2px}
.fm-link:focus-visible{outline:2px solid var(--fm-accent);outline-offset:2px}
/* 1. Sign in */
.fm.is-auth{--fm-w:420px}
.fm-auth-head{padding:34px 32px 0;text-align:center}
.fm-logo{display:grid;place-items:center;width:48px;height:48px;margin:0 auto 14px;border-radius:14px;background:linear-gradient(145deg,#14b8a6,#0f766e);color:#fff;
box-shadow:0 8px 18px -8px rgba(15,118,110,.8)}
.fm-logo svg{width:26px;height:26px}
.fm-or{display:flex;align-items:center;gap:12px;margin:18px 0;color:#5b6474;font-size:.75rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
.fm-or::before,.fm-or::after{content:"";flex:1;height:1px;background:var(--fm-line)}
.fm-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 16px;margin:0 0 24px;font-size:.875rem}
.fm-note{margin:0;padding:16px 32px;border-top:1px solid var(--fm-line);background:var(--fm-soft);color:var(--fm-muted);font-size:.875rem;text-align:center}
/* 2. Newsletter: photo left, form right; stacked with a short photo band on phones */
.fm.is-news{--fm-w:760px}
.fm-form.fm-news{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr)}
.fm-news-media{position:relative;min-height:180px;margin:0;background:#d9d2c3}
.fm-news-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.fm-news-body{align-self:center;padding:48px 40px 40px}
.fm-eyebrow{margin:0 0 10px;color:var(--fm-accent);font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
.fm-news .fm-title{font-size:1.6rem;line-height:1.2;letter-spacing:-.02em}
.fm-lead{margin:10px 0 22px;color:var(--fm-muted)}
.fm-inline{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
.fm-inline .fm-input{flex:1 1 190px;width:auto;min-width:0}
.fm-fine{margin:12px 0 0;color:#5b6474;font-size:.8rem}
/* 3. Feedback with a star rating */
.fm.is-feedback{--fm-w:500px}
.fm-head{display:flex;align-items:center;gap:14px;padding:24px 60px 4px 24px}
.fm-avatar{flex:none;width:48px;height:48px;border-radius:50%;object-fit:cover;box-shadow:0 0 0 3px #fff,0 0 0 4px var(--fm-line)}
.fm-head .fm-title{font-size:1.1rem;line-height:1.35}
.fm-head .fm-sub{margin-top:2px;font-size:.85rem}
.fm.is-feedback .fm-body{padding:20px 24px 22px}
.fm-set{min-width:0;margin:0 0 24px;padding:0;border:0}
.fm-set legend{display:block;margin:0 0 8px;padding:0}
.fm-hint{margin-left:6px;color:#5b6474;font-size:.8rem;font-weight:400}
/* The stars: visually hidden radios inside labels. --fm-rate (checked) and --fm-hover (hovered) set how
many stars are filled; each star fills when its number --i is within that count. */
.fm-stars{--fm-show:var(--fm-hover,var(--fm-rate,0));display:flex;margin-left:-6px}
.fm-stars:has(input[value="1"]:checked){--fm-rate:1}
.fm-stars:has(input[value="2"]:checked){--fm-rate:2}
.fm-stars:has(input[value="3"]:checked){--fm-rate:3}
.fm-stars:has(input[value="4"]:checked){--fm-rate:4}
.fm-stars:has(input[value="5"]:checked){--fm-rate:5}
.fm-stars:has(.fm-star:nth-child(1):hover){--fm-hover:1}
.fm-stars:has(.fm-star:nth-child(2):hover){--fm-hover:2}
.fm-stars:has(.fm-star:nth-child(3):hover){--fm-hover:3}
.fm-stars:has(.fm-star:nth-child(4):hover){--fm-hover:4}
.fm-stars:has(.fm-star:nth-child(5):hover){--fm-hover:5}
.fm-star{display:grid;place-items:center;width:46px;height:46px;border-radius:10px;cursor:pointer}
.fm-star:nth-child(2){--i:2}.fm-star:nth-child(3){--i:3}.fm-star:nth-child(4){--i:4}.fm-star:nth-child(5){--i:5}
.fm-star svg{width:34px;height:34px;fill:#f59e0b;fill-opacity:clamp(0,calc(var(--fm-show) - var(--i,1) + 1),1);stroke:#b45309;stroke-width:1.4;stroke-linejoin:round;
transition:fill-opacity .15s,transform .15s}
.fm-star:hover svg{transform:scale(1.12)}
.fm-star:has(:focus-visible){outline:2px solid var(--fm-accent);outline-offset:-2px}
/* Optional chips: checkbox pills */
.fm-chips{display:flex;flex-wrap:wrap;gap:8px}
.fm-chip{display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:6px 14px 6px 11px;border:1px solid #d4cdbf;border-radius:999px;background:#fff;color:#334155;
font-size:.85rem;font-weight:500;cursor:pointer;transition:background-color .15s,border-color .15s,color .15s}
.fm-chip svg{width:16px;height:16px;flex:none}
.fm-chip .is-on{display:none}
.fm-chip:hover{border-color:#a8a090;background:var(--fm-soft)}
.fm-chip:has(:checked){border-color:var(--fm-accent);background:#e6f4f1;color:var(--fm-accent-dark)}
.fm-chip:has(:checked) .is-off{display:none}
.fm-chip:has(:checked) .is-on{display:block}
.fm-chip:has(:focus-visible){outline:2px solid var(--fm-accent);outline-offset:2px}
.fm.is-feedback .fm-field{margin:0}
@media (max-width:767.98px){
.fm-form.fm-news{grid-template-columns:minmax(0,1fr)}
.fm-news-media{height:140px;min-height:0}
.fm-news-media img{object-position:center 62%}
.fm-news-body{padding:22px 20px 24px}
.fm-news .fm-title{font-size:1.35rem}
}
@media (max-width:600px){
.fm{max-height:calc(100dvh - 32px)}
.fm-auth-head{padding:30px 20px 0}
.fm-body{padding:20px 20px 22px}
.fm-note{padding:14px 20px}
.fm-head{padding:20px 56px 2px 18px}
.fm.is-feedback .fm-body{padding:18px 18px 20px}
.fm-foot{padding:14px 18px}
.fm-foot .fm-btn,.fm-inline .fm-btn{flex:1 1 auto}
}
@media (prefers-reduced-motion:reduce){
.fm[open],.fm[open]::backdrop{animation:none}
.fm-btn,.fm-x,.fm-input,.fm-check input,.fm-chip,.fm-star svg{transition:none}
.fm-star:hover svg{transform:none}
}
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script;
// the browser checks required and type="email" first, so an invalid form stays open.
document.querySelectorAll('[data-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
});
// A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});
/* Bootstrap's Modal, .form-control, .form-check and .btn-check, restyled through --bs-* variables. */
.fm-btn{--bs-btn-padding-x:18px;--bs-btn-padding-y:10px;--bs-btn-font-size:.9rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.2;--bs-btn-border-radius:10px;
--bs-btn-bg:#fff;--bs-btn-color:#0f172a;--bs-btn-border-color:#d4cdbf;--bs-btn-hover-bg:#f7f4ee;--bs-btn-hover-color:#0f172a;--bs-btn-hover-border-color:#a8a090;
--bs-btn-active-bg:#efeae0;--bs-btn-active-color:#0f172a;--bs-btn-active-border-color:#a8a090;
display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px}
.fm-btn.btn-primary{--bs-btn-bg:#0f766e;--bs-btn-color:#fff;--bs-btn-border-color:#0f766e;--bs-btn-hover-bg:#115e59;--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:#115e59;
--bs-btn-active-bg:#134e4a;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#134e4a;box-shadow:0 6px 16px -8px rgba(15,118,110,.75)}
.fm-btn:focus-visible{outline:2px solid #0f766e;outline-offset:2px;box-shadow:none}
.fm-btn svg{width:18px;height:18px;flex:none}
.fm{--bs-modal-width:440px;--bs-modal-margin:1rem;--bs-modal-color:#0f172a;--bs-modal-bg:#fff;--bs-modal-border-width:0;--bs-modal-border-radius:18px;--bs-modal-inner-border-radius:18px;
--bs-modal-header-border-width:0;--bs-modal-title-line-height:1.3;--bs-modal-footer-bg:#faf8f4;--bs-modal-footer-border-color:#e8e3d9;
font-size:.95rem;line-height:1.55}
@media (min-width:576px){.fm{--bs-modal-margin:1.75rem}}
.fm .modal-content{overflow:hidden;box-shadow:0 0 0 1px rgba(15,23,42,.06),0 28px 64px -18px rgba(15,23,42,.45)}
.fm .modal-body{padding:22px 32px 26px}
.fm .modal-title{font-size:1.3rem;font-weight:700;letter-spacing:-.015em}
.fm-sub{margin:4px 0 0;color:#475569;font-size:.9rem}
.fm .modal-footer{gap:10px;padding:16px 24px}
.fm .modal-footer>*{margin:0}
.fm .btn-close{position:absolute;top:12px;right:12px;z-index:2;width:36px;height:36px;padding:0;border-radius:10px;background-size:13px;opacity:.75}
.fm .btn-close:hover{background-color:#f3efe7;opacity:1}
.fm .btn-close:focus-visible{outline:2px solid #0f766e;outline-offset:1px;box-shadow:none;opacity:1}
/* Over a photo the X sits on a solid, slightly blurred chip so it keeps its contrast */
.fm .btn-close.fm-chip-x{border-radius:50%;background-color:rgba(255,255,255,.9);opacity:1;box-shadow:0 1px 3px rgba(15,23,42,.2);backdrop-filter:blur(6px)}
.fm .btn-close.fm-chip-x:hover{background-color:#fff}
/* Same entrance as the plain version: a short rise and scale instead of Bootstrap's 50px drop */
.fm.fade .modal-dialog{transform:translateY(10px) scale(.97);transition:transform .22s cubic-bezier(.2,.8,.2,1)}
.fm.show .modal-dialog{transform:none}
.modal-backdrop{--bs-backdrop-bg:#0f172a;--bs-backdrop-opacity:.55;backdrop-filter:blur(2px)}
/* Fields: 44px tall, 10px radius, the label always above the field */
.fm .form-label{display:block;margin-bottom:6px;color:#0f172a;font-size:.85rem;font-weight:600;line-height:1.3}
.fm legend.form-label{float:none;width:auto;margin-bottom:8px;font-size:.85rem}
.fm-opt,.fm-hint{color:#5b6474;font-weight:400}
.fm-hint{margin-left:6px;font-size:.8rem}
.fm .form-control{height:44px;padding:0 14px;border-color:#848e9d;border-radius:10px;color:#0f172a;font-size:.95rem;transition:border-color .15s,box-shadow .15s}
.fm .form-control::placeholder{color:#6b7280}
.fm .form-control:hover{border-color:#5f6b7b}
.fm .form-control:focus{border-color:#0f766e;box-shadow:0 0 0 3px rgba(15,118,110,.24)}
.fm .form-control:user-invalid{border-color:#b91c1c}
.fm .form-control:user-invalid:focus{box-shadow:0 0 0 3px rgba(185,28,28,.2)}
.fm textarea.form-control{height:auto;min-height:104px;padding:10px 14px;line-height:1.5} /* undo the 44px height and Bootstrap's one-line min-height */
.fm .form-check{display:flex;align-items:center;gap:8px;min-height:0;margin:0;padding:0;color:#334155}
.fm .form-check-input{float:none;flex:none;width:18px;height:18px;margin:0;border:1.5px solid #848e9d;border-radius:5px;background-size:12px;cursor:pointer}
.fm .form-check-input:checked{background-color:#0f766e;border-color:#0f766e}
.fm .form-check-input:focus{border-color:#848e9d;box-shadow:none}
.fm .form-check-input:checked:focus{border-color:#0f766e}
.fm .form-check-input:focus-visible{outline:2px solid #0f766e;outline-offset:2px}
.fm .form-check-label{cursor:pointer}
.fm-link{border-radius:4px;color:#0f766e;font-weight:600;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}
.fm-link:hover{color:#115e59;text-decoration-thickness:2px}
.fm-link:focus-visible{outline:2px solid #0f766e;outline-offset:2px}
/* 1. Sign in */
.fm-auth{--bs-modal-width:420px}
.fm-auth .modal-header{display:block;padding:34px 32px 0;text-align:center}
.fm-logo{display:grid;place-items:center;width:48px;height:48px;margin:0 auto 14px;border-radius:14px;background:linear-gradient(145deg,#14b8a6,#0f766e);color:#fff;
box-shadow:0 8px 18px -8px rgba(15,118,110,.8)}
.fm-logo svg{width:26px;height:26px}
.fm-or{display:flex;align-items:center;gap:12px;margin:18px 0;color:#5b6474;font-size:.75rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
.fm-or::before,.fm-or::after{content:"";flex:1;height:1px;background:#e8e3d9}
.fm-auth .form-check,.fm-auth .fm-link{font-size:.875rem}
.fm-auth .modal-footer{justify-content:center;padding:16px 32px;color:#475569;font-size:.875rem;text-align:center}
/* 2. Newsletter: photo left, form right; stacked with a short photo band on phones */
.fm-news{--bs-modal-width:760px}
.fm-news-media{position:relative;min-height:180px;background:#d9d2c3}
.fm-news-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.fm-news-body{padding:48px 40px 40px}
.fm-eyebrow{margin:0 0 10px;color:#0f766e;font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
.fm-news .modal-title{font-size:1.6rem;line-height:1.2;letter-spacing:-.02em}
.fm-lead{margin:10px 0 22px;color:#475569}
.fm-news .form-control{flex:1 1 190px;width:auto;min-width:0}
.fm-fine{margin:12px 0 0;color:#5b6474;font-size:.8rem}
/* 3. Feedback with a star rating */
.fm-feedback{--bs-modal-width:500px}
.fm-feedback .modal-header{gap:14px;padding:24px 60px 4px 24px}
.fm-avatar{flex:none;width:48px;height:48px;border-radius:50%;object-fit:cover;box-shadow:0 0 0 3px #fff,0 0 0 4px #e8e3d9}
.fm-feedback .modal-title{font-size:1.1rem;line-height:1.35}
.fm-feedback .fm-sub{margin-top:2px;font-size:.85rem}
.fm-feedback .modal-body{padding:20px 24px 22px}
/* The stars: visually hidden radios inside labels. --fm-rate (checked) and --fm-hover (hovered) set how
many stars are filled; each star fills when its number --i is within that count. */
.fm-stars{--fm-show:var(--fm-hover,var(--fm-rate,0));display:flex;margin-left:-6px}
.fm-stars:has(input[value="1"]:checked){--fm-rate:1}
.fm-stars:has(input[value="2"]:checked){--fm-rate:2}
.fm-stars:has(input[value="3"]:checked){--fm-rate:3}
.fm-stars:has(input[value="4"]:checked){--fm-rate:4}
.fm-stars:has(input[value="5"]:checked){--fm-rate:5}
.fm-stars:has(.fm-star:nth-child(1):hover){--fm-hover:1}
.fm-stars:has(.fm-star:nth-child(2):hover){--fm-hover:2}
.fm-stars:has(.fm-star:nth-child(3):hover){--fm-hover:3}
.fm-stars:has(.fm-star:nth-child(4):hover){--fm-hover:4}
.fm-stars:has(.fm-star:nth-child(5):hover){--fm-hover:5}
.fm-star{display:grid;place-items:center;width:46px;height:46px;border-radius:10px;cursor:pointer}
.fm-star:nth-child(2){--i:2}.fm-star:nth-child(3){--i:3}.fm-star:nth-child(4){--i:4}.fm-star:nth-child(5){--i:5}
.fm-star svg{width:34px;height:34px;fill:#f59e0b;fill-opacity:clamp(0,calc(var(--fm-show) - var(--i,1) + 1),1);stroke:#b45309;stroke-width:1.4;stroke-linejoin:round;
transition:fill-opacity .15s,transform .15s}
.fm-star:hover svg{transform:scale(1.12)}
.fm-star:has(:focus-visible){outline:2px solid #0f766e;outline-offset:-2px}
/* Optional chips: .btn-check checkboxes styled as pills */
.fm-chip{--bs-btn-padding-x:14px;--bs-btn-padding-y:6px;--bs-btn-font-size:.85rem;--bs-btn-font-weight:500;--bs-btn-border-radius:999px;
--bs-btn-bg:#fff;--bs-btn-color:#334155;--bs-btn-border-color:#d4cdbf;--bs-btn-hover-bg:#faf8f4;--bs-btn-hover-color:#334155;--bs-btn-hover-border-color:#a8a090;
--bs-btn-active-bg:#e6f4f1;--bs-btn-active-color:#115e59;--bs-btn-active-border-color:#0f766e;
display:inline-flex;align-items:center;gap:6px;min-height:38px;padding-left:11px}
.fm-chip svg{width:16px;height:16px;flex:none}
.fm-chip .is-on,.btn-check:checked+.fm-chip .is-off{display:none}
.btn-check:checked+.fm-chip .is-on{display:block}
.btn-check:focus-visible+.fm-chip{outline:2px solid #0f766e;outline-offset:2px;box-shadow:none}
@media (max-width:767.98px){
.fm-news-media{height:140px;min-height:0}
.fm-news-media img{object-position:center 62%}
.fm-news-body{padding:22px 20px 24px}
.fm-news .modal-title{font-size:1.35rem}
}
@media (max-width:600px){
.fm-auth .modal-header{padding:30px 20px 0}
.fm .modal-body{padding:20px 20px 22px}
.fm-auth .modal-footer{padding:14px 20px}
.fm-feedback .modal-header{padding:20px 56px 2px 18px}
.fm-feedback .modal-body{padding:18px 18px 20px}
.fm .modal-footer{padding-left:18px;padding-right:18px}
.fm-feedback .modal-footer .fm-btn,.fm-news .fm-btn{flex:1 1 auto}
}
@media (prefers-reduced-motion:reduce){
.fm.fade .modal-dialog,.fm-btn,.fm .form-control,.fm-star svg{transition:none}
.fm-star:hover svg{transform:none}
}
JavaScript
// An invalid form never fires submit: the browser stops it and shows its own message (required, type="email").
// A valid one would reload the page, so stop that and close the modal instead (send the data with fetch here).
document.querySelectorAll('.fm form').forEach(function (form) {
form.addEventListener('submit', function (e) {
if (!form.checkValidity()) return; // let the browser report the problem
e.preventDefault();
bootstrap.Modal.getOrCreateInstance(form.closest('.modal')).hide();
});
});
// Bootstrap focuses the modal box itself when it opens; move focus to the field marked autofocus.
document.querySelectorAll('.fm.modal').forEach(function (modal) {
modal.addEventListener('shown.bs.modal', function () {
var field = modal.querySelector('[autofocus]');
if (field) field.focus();
});
});
/* Entrance keyframes for the dialog and its backdrop (used through motion-safe:open:animate-[…]) */
@keyframes fm-in { from { opacity: 0; transform: translateY(10px) scale(.97); } }
@keyframes fm-fade { from { opacity: 0; } }
/* Stop the page scrolling behind an open modal */
html:has(dialog:modal) { overflow: hidden; }
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script;
// the browser checks required and type="email" first, so an invalid form stays open.
document.querySelectorAll('[data-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
});
// A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});
About this set
Form Modals is three dialogs for a note-taking app called Fieldnote, each built around a real form. Login / sign up is a centred sign-in card with a Continue with SSO button, an or divider, labelled email and password fields, Remember me, a Forgot password link and a Create an account footer. Newsletter is a two-column subscribe popup with a photo on the left that turns into a short photo band on phones, with an inline email field and Subscribe button. Feedback with rating asks how an onboarding call went, with a five-star rating, optional checkbox chips and a comment box. Use the first behind a sign-in button, the second for an opt-in prompt and the third after a finished task. In the plain CSS and Tailwind versions each dialog holds a form with method=dialog, so the browser’s own required and type=email checks must pass before the submit button closes it, while Not now and the X use formnovalidate. The Bootstrap version uses the Modal plugin with .form-control, .form-check and .btn-check, plus a few lines that hide the modal only after a valid submit. The stars are visually hidden radios: :has() works out how many are checked or hovered, and each star’s fill-opacity follows that number, with no script.
What’s included
Native validation: required and type=email keep the dialog open until the form is valid
44px fields with labels above them, a visible focus ring and a red border on invalid input
Five-star rating from radio inputs, filled on hover and on check with CSS only
Checkbox pill chips (.btn-check in Bootstrap) that swap a plus icon for a check mark
Newsletter layout that stacks into a photo band on phones, with a close button on a solid chip
Accessibility
Each dialog is named by its visible heading through aria-labelledby, and the newsletter is also described by its intro sentence. Every field has a visible label and the right autocomplete token; the rating is a fieldset with a legend whose visually hidden radios are named 1 star to 5 stars, so arrow keys move through them and the focused star gets a ring. Focus goes to the email field when the sign-in and newsletter dialogs open (in Bootstrap through a shown.bs.modal listener) and back to the trigger when any dialog closes. The entrance animation, transitions and the star hover scale are switched off under prefers-reduced-motion.
Customise it
In the plain version change --fm-accent, --fm-field (the input border) and --fm-w on .fm. In Bootstrap edit --bs-modal-width and the --bs-btn-* values on .fm-btn and .fm-chip, plus the .form-control rules in the style block; in Tailwind replace #0f766e and the w-[min(…)] width in the class lists.