public
imalexnord
read
Slot
Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.
Languages
Repository composition by tracked source files.
TypeScript
79%
CSS
19%
SQL
2%
HTML
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
Commit
Make booking frame stateful
b8c34b1
src/App.tsx | 380 +++++++++++++++++++++++++++++----------------------------
src/styles.css | 168 ++++++++++++++++++++-----
2 files changed, 330 insertions(+), 218 deletions(-)
Diff
diff --git a/src/App.tsx b/src/App.tsx
index a3ab9db..5c08e9e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -220,6 +220,7 @@ function PublicBooking() {
setSelectedDate(key);
setSelectedSlot(null);
setDuration(null);
+ setConfirmation(null);
setSlots([]);
setError('');
setAvailabilityNotice('');
@@ -273,41 +274,212 @@ function PublicBooking() {
return <div className="center-screen"><Loader2 className="spin" /> Loading calendar…</div>;
}
- if (confirmation) {
+ const activeConfig = config;
+ const chosenDurations = selectedSlot?.durations ?? [];
+ const bookingStep = confirmation
+ ? 'confirmed'
+ : !selectedSlot
+ ? 'time'
+ : !duration
+ ? 'duration'
+ : 'details';
+
+ function changeTime() {
+ setSelectedSlot(null);
+ setDuration(null);
+ setConfirmation(null);
+ }
+
+ function changeDuration() {
+ setDuration(null);
+ setConfirmation(null);
+ }
+
+ function bookAnotherTime() {
+ setSelectedSlot(null);
+ setDuration(null);
+ setConfirmation(null);
+ setError('');
+ }
+
+ function renderTimesView() {
return (
- <main className="confirmation-shell">
- <Logo />
- <section className="confirmation-card">
- <span className="success-icon"><Check /></span>
- <p className="eyebrow">YOU'RE BOOKED</p>
- <h1>See you then.</h1>
- <p className="confirmation-time">{formatInstant(confirmation.start, config.timezone)} · {confirmation.duration} min</p>
- <p className="muted">A calendar invitation has been sent to your email.</p>
- {confirmation.meetUrl && (
- <a className="primary-button inline-button" href={confirmation.meetUrl} target="_blank" rel="noreferrer">
- Open Google Meet <ExternalLink size={16} />
- </a>
+ <div className="panel-view">
+ <div className="selection-heading">
+ <p className="step-label">02 · PICK A TIME</p>
+ <h2>{prettyDate(selectedDate, activeConfig.timezone)}</h2>
+ </div>
+
+ {loadingSlots ? (
+ <div className="loading-row"><Loader2 className="spin" /> Checking Google Calendar...</div>
+ ) : availabilityNotice ? (
+ <div className="no-slots">{availabilityNotice}</div>
+ ) : slots.length === 0 ? (
+ <div className="no-slots">Nothing open on this day.</div>
+ ) : (
+ <div className="time-grid time-list">
+ {slots.map((slot) => (
+ <button
+ key={slot.start}
+ className={`time-button ${selectedSlot?.start === slot.start ? 'active' : ''}`}
+ onClick={() => {
+ setSelectedSlot(slot);
+ setDuration(null);
+ setConfirmation(null);
+ }}
+ >
+ {slot.time}
+ </button>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ function renderDurationView() {
+ if (!selectedSlot) return null;
+ return (
+ <div className="panel-view">
+ <button className="panel-back" type="button" onClick={changeTime}>
+ <ArrowLeft size={16} /> Change time
+ </button>
+ <div className="selected-summary">
+ <span>{prettyDate(selectedDate, activeConfig.timezone)}</span>
+ <strong>{selectedSlot.time}</strong>
+ </div>
+ <div className="duration-picker-view">
+ <p className="step-label">03 · HOW LONG?</p>
+ <div className="duration-grid">
+ {chosenDurations.map((minutes) => (
+ <button
+ key={minutes}
+ className={`duration-button ${duration === minutes ? 'active' : ''}`}
+ onClick={() => setDuration(minutes)}
+ >
+ <strong>{minutes}</strong>
+ <span>min</span>
+ </button>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ function renderDetailsView() {
+ if (!selectedSlot || !duration) return null;
+ return (
+ <div className="panel-view panel-view-scroll">
+ <button className="panel-back" type="button" onClick={changeDuration}>
+ <ArrowLeft size={16} /> Change duration
+ </button>
+ <div className="selected-summary">
+ <span>{prettyDate(selectedDate, activeConfig.timezone)}</span>
+ <strong>{selectedSlot.time} · {duration} min</strong>
+ </div>
+ <form className="details-form compact-details" onSubmit={submitBooking}>
+ <p className="step-label">04 · YOUR DETAILS</p>
+ <div className="form-row">
+ <label>Name<input name="name" required autoComplete="name" placeholder="Your name" /></label>
+ <label>Email<input name="email" type="email" required autoComplete="email" placeholder="[email protected]" /></label>
+ </div>
+
+ {activeConfig.allowMeetingModes.length > 1 && (
+ <div className="mode-picker">
+ <span className="field-label">Meeting</span>
+ <div className="mode-options">
+ {activeConfig.allowMeetingModes.map((mode) => {
+ const Icon = modeMeta[mode].icon;
+ return (
+ <button
+ key={mode}
+ type="button"
+ className={`mode-option ${meetingMode === mode ? 'active' : ''}`}
+ onClick={() => setMeetingMode(mode)}
+ >
+ <Icon size={18} />
+ <span><strong>{modeMeta[mode].label}</strong><small>{modeMeta[mode].help}</small></span>
+ </button>
+ );
+ })}
+ </div>
+ </div>
)}
- <button className="text-button" onClick={() => window.location.reload()}>Book another time</button>
- </section>
- </main>
+
+ {meetingMode === 'phone' && (
+ <label>Phone number<input name="phone" required autoComplete="tel" placeholder="+46 ..." /></label>
+ )}
+ <label>Anything I should know? <span className="optional">Optional</span>
+ <textarea name="message" rows={3} placeholder="Context, topic, links..." />
+ </label>
+ <label className="honeypot" aria-hidden="true">Website<input name="website" tabIndex={-1} autoComplete="off" /></label>
+
+ {error && <div className="error-banner">{error}</div>}
+ <button className="primary-button" type="submit" disabled={booking}>
+ {booking ? <><Loader2 className="spin" size={18} /> Booking...</> : <>Book this slot <ArrowRight size={18} /></>}
+ </button>
+ </form>
+ </div>
);
}
- const chosenDurations = selectedSlot?.durations ?? [];
+ function renderConfirmationView() {
+ if (!confirmation) return null;
+ return (
+ <div className="panel-view confirmation-panel">
+ <span className="success-icon"><Check /></span>
+ <p className="eyebrow">YOU'RE BOOKED</p>
+ <h2>See you then.</h2>
+ <p className="confirmation-time">{formatInstant(confirmation.start, activeConfig.timezone)} · {confirmation.duration} min</p>
+ <p className="muted">A calendar invitation has been sent to your email.</p>
+ {confirmation.meetUrl && (
+ <a className="primary-button inline-button" href={confirmation.meetUrl} target="_blank" rel="noreferrer">
+ Open Google Meet <ExternalLink size={16} />
+ </a>
+ )}
+ <button className="text-button" type="button" onClick={bookAnotherTime}>Book another time</button>
+ </div>
+ );
+ }
+
+ function renderRightPanel() {
+ if (!activeConfig.connected) {
+ return (
+ <div className="empty-state">
+ <MonitorUp size={28} />
+ <h3>Calendar is not connected yet</h3>
+ <p>The owner needs to connect Google Calendar before times can be booked.</p>
+ </div>
+ );
+ }
+ if (!selectedDate) {
+ return (
+ <div className="empty-state subtle-empty">
+ <ArrowLeft size={28} />
+ <h3>Start with the calendar</h3>
+ <p>Pick a date and available times will appear here.</p>
+ </div>
+ );
+ }
+ if (bookingStep === 'confirmed') return renderConfirmationView();
+ if (bookingStep === 'duration') return renderDurationView();
+ if (bookingStep === 'details') return renderDetailsView();
+ return renderTimesView();
+ }
return (
<main className="booking-page">
<header className="topbar">
- <span className="timezone-pill"><Clock3 size={14} /> {config.timezone}</span>
+ <span className="timezone-pill"><Clock3 size={14} /> {activeConfig.timezone}</span>
</header>
<section className="booking-shell">
<aside className="intro-panel">
<div>
<p className="eyebrow">SCHEDULE A MEETING</p>
- <h1>{config.title}</h1>
- <p className="intro-copy">{config.subtitle}</p>
+ <h1>{activeConfig.title}</h1>
+ <p className="intro-copy">{activeConfig.subtitle}</p>
</div>
<div className="intro-note">
<CalendarDays size={18} />
@@ -336,7 +508,7 @@ function PublicBooking() {
<div className="calendar-grid">
{cells.map(({ date, current }) => {
const key = dateKey(date);
- const disabled = !current || key < today || key > maxDate || !config.availableWeekdays.includes(date.getDay());
+ const disabled = !current || key < today || key > maxDate || !activeConfig.availableWeekdays.includes(date.getDay());
const selected = key === selectedDate;
const isToday = key === today;
return (
@@ -354,173 +526,9 @@ function PublicBooking() {
</div>
</section>
- <aside className="selection-panel">
- {!config.connected ? (
- <div className="empty-state">
- <MonitorUp size={28} />
- <h3>Calendar is not connected yet</h3>
- <p>The owner needs to connect Google Calendar before times can be booked.</p>
- </div>
- ) : !selectedDate ? (
- <div className="empty-state subtle-empty">
- <ArrowLeft size={28} />
- <h3>Start with the calendar</h3>
- <p>Pick a date and available times will appear here.</p>
- </div>
- ) : (
- <>
- <div className="selection-heading">
- <p className="step-label">02 · PICK A TIME</p>
- <h2>{prettyDate(selectedDate, config.timezone)}</h2>
- </div>
-
- {loadingSlots ? (
- <div className="loading-row"><Loader2 className="spin" /> Checking Google Calendar…</div>
- ) : availabilityNotice ? (
- <div className="no-slots">{availabilityNotice}</div>
- ) : slots.length === 0 ? (
- <div className="no-slots">Nothing open on this day.</div>
- ) : (
- <div className="time-grid">
- {slots.map((slot) => (
- <button
- key={slot.start}
- className={`time-button ${selectedSlot?.start === slot.start ? 'active' : ''}`}
- onClick={() => {
- setSelectedSlot(slot);
- setDuration(null);
- }}
- >
- {slot.time}
- </button>
- ))}
- </div>
- )}
-
- {selectedSlot && (
- <div className="duration-section">
- <p className="step-label">03 · HOW LONG?</p>
- <div className="duration-grid">
- {chosenDurations.map((minutes) => (
- <button
- key={minutes}
- className={`duration-button ${duration === minutes ? 'active' : ''}`}
- onClick={() => setDuration(minutes)}
- >
- <strong>{minutes}</strong>
- <span>min</span>
- </button>
- ))}
- </div>
- </div>
- )}
-
- {selectedSlot && duration && (
- <div className="inline-details">
- <div className="drawer-summary">
- <p className="step-label">04 · YOUR DETAILS</p>
- <h2>{selectedSlot.time} · {duration} minutes</h2>
- <p>{prettyDate(selectedDate, config.timezone)}</p>
- </div>
- <form className="details-form" onSubmit={submitBooking}>
- <div className="form-row">
- <label>Name<input name="name" required autoComplete="name" placeholder="Your name" /></label>
- <label>Email<input name="email" type="email" required autoComplete="email" placeholder="[email protected]" /></label>
- </div>
-
- {config.allowMeetingModes.length > 1 && (
- <div className="mode-picker">
- <span className="field-label">Meeting</span>
- <div className="mode-options">
- {config.allowMeetingModes.map((mode) => {
- const Icon = modeMeta[mode].icon;
- return (
- <button
- key={mode}
- type="button"
- className={`mode-option ${meetingMode === mode ? 'active' : ''}`}
- onClick={() => setMeetingMode(mode)}
- >
- <Icon size={18} />
- <span><strong>{modeMeta[mode].label}</strong><small>{modeMeta[mode].help}</small></span>
- </button>
- );
- })}
- </div>
- </div>
- )}
-
- {meetingMode === 'phone' && (
- <label>Phone number<input name="phone" required autoComplete="tel" placeholder="+46 ..." /></label>
- )}
- <label>Anything I should know? <span className="optional">Optional</span>
- <textarea name="message" rows={3} placeholder="Context, topic, links..." />
- </label>
- <label className="honeypot" aria-hidden="true">Website<input name="website" tabIndex={-1} autoComplete="off" /></label>
-
- {error && <div className="error-banner">{error}</div>}
- <button className="primary-button" type="submit" disabled={booking}>
- {booking ? <><Loader2 className="spin" size={18} /> Booking...</> : <>Book this time <ArrowRight size={18} /></>}
- </button>
- </form>
- </div>
- )}
- </>
- )}
- </aside>
+ <aside className="selection-panel">{renderRightPanel()}</aside>
</section>
- {selectedSlot && duration && (
- <section className="details-drawer">
- <div className="drawer-summary">
- <p className="step-label">04 · YOUR DETAILS</p>
- <h2>{selectedSlot.time} · {duration} minutes</h2>
- <p>{prettyDate(selectedDate, config.timezone)}</p>
- </div>
- <form className="details-form" onSubmit={submitBooking}>
- <div className="form-row">
- <label>Name<input name="name" required autoComplete="name" placeholder="Your name" /></label>
- <label>Email<input name="email" type="email" required autoComplete="email" placeholder="[email protected]" /></label>
- </div>
-
- {config.allowMeetingModes.length > 1 && (
- <div className="mode-picker">
- <span className="field-label">Meeting</span>
- <div className="mode-options">
- {config.allowMeetingModes.map((mode) => {
- const Icon = modeMeta[mode].icon;
- return (
- <button
- key={mode}
- type="button"
- className={`mode-option ${meetingMode === mode ? 'active' : ''}`}
- onClick={() => setMeetingMode(mode)}
- >
- <Icon size={18} />
- <span><strong>{modeMeta[mode].label}</strong><small>{modeMeta[mode].help}</small></span>
- </button>
- );
- })}
- </div>
- </div>
- )}
-
- {meetingMode === 'phone' && (
- <label>Phone number<input name="phone" required autoComplete="tel" placeholder="+46 …" /></label>
- )}
- <label>Anything I should know? <span className="optional">Optional</span>
- <textarea name="message" rows={3} placeholder="Context, topic, links…" />
- </label>
- <label className="honeypot" aria-hidden="true">Website<input name="website" tabIndex={-1} autoComplete="off" /></label>
-
- {error && <div className="error-banner">{error}</div>}
- <button className="primary-button" type="submit" disabled={booking}>
- {booking ? <><Loader2 className="spin" size={18} /> Booking…</> : <>Book this time <ArrowRight size={18} /></>}
- </button>
- </form>
- </section>
- )}
-
{error && !(selectedSlot && duration) && <div className="floating-error">{error}</div>}
<footer>
<a className="powered-by" href="https://www.nobgit.com/user/imalexnord/slot" target="_blank" rel="noreferrer">Powered by Slot</a>
diff --git a/src/styles.css b/src/styles.css
index c71c380..2b2e73a 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -192,8 +192,18 @@
}
* { box-sizing: border-box; }
-html { min-height: 100%; background: var(--bg); }
-body { margin: 0; min-width: 320px; min-height: 100vh; background: var(--bg); color: var(--text); }
+html, body, #root {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+html { background: var(--bg); }
+body {
+ min-width: 320px;
+ overflow: hidden;
+ background: var(--bg);
+ color: var(--text);
+}
button, input, textarea { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
button { color: inherit; }
@@ -234,14 +244,15 @@ a { color: inherit; text-decoration: none; }
}
.booking-page {
- min-height: 100svh;
- padding: 0 26px;
+ width: 100vw;
+ height: 100dvh;
+ min-height: 0;
+ padding: 16px;
position: relative;
display: flex;
- flex-direction: column;
justify-content: center;
align-items: center;
- gap: 14px;
+ overflow: hidden;
}
.topbar, .admin-topbar {
max-width: 1320px;
@@ -273,12 +284,14 @@ a { color: inherit; text-decoration: none; }
}
.booking-shell {
- width: 100%;
- max-width: 1320px;
+ width: min(1180px, calc(100vw - 32px));
+ height: min(760px, calc(100dvh - 88px));
+ max-width: calc(100vw - 32px);
+ max-height: calc(100dvh - 88px);
margin: 0;
display: grid;
- grid-template-columns: minmax(260px, 1fr) minmax(540px, 680px) minmax(260px, 1fr);
- min-height: 680px;
+ grid-template-columns: minmax(230px, .78fr) minmax(500px, 1.55fr) minmax(270px, .88fr);
+ min-height: 0;
border: 1px solid var(--border);
border-radius: 26px;
background: var(--surface);
@@ -288,7 +301,8 @@ a { color: inherit; text-decoration: none; }
.booking-shell > * + * { border-left: 1px solid var(--border); }
.intro-panel {
- padding: 48px 38px 36px;
+ min-height: 0;
+ padding: 48px 34px 36px;
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -329,7 +343,12 @@ a { color: inherit; text-decoration: none; }
.intro-note div { display: grid; gap: 4px; }
.intro-note strong { color: var(--text); font-size: 12px; }
-.calendar-panel { padding: 38px 40px 42px; }
+.calendar-panel {
+ min-height: 0;
+ padding: 38px 40px 42px;
+ display: flex;
+ flex-direction: column;
+}
.calendar-toolbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 22px; margin-bottom: 34px; }
.calendar-toolbar h2, .selection-heading h2, .drawer-summary h2, .settings-card h2 {
margin: 0;
@@ -359,10 +378,14 @@ a { color: inherit; text-decoration: none; }
letter-spacing: .12em;
padding: 7px 0;
}
-.calendar-grid { gap: 7px; }
+.calendar-grid {
+ flex: 1;
+ min-height: 0;
+ grid-template-rows: repeat(6, minmax(0, 1fr));
+ gap: 7px;
+}
.day {
- aspect-ratio: 1.02;
- min-height: 62px;
+ min-height: 0;
border: 1px solid transparent;
background: transparent;
border-radius: 14px;
@@ -388,11 +411,60 @@ a { color: inherit; text-decoration: none; }
.selection-panel {
padding: 38px 34px;
background: color-mix(in srgb, var(--surface-soft) 52%, var(--surface));
- overflow-y: auto;
+ overflow: hidden;
min-height: 0;
+ display: flex;
+ flex-direction: column;
}
.selection-heading { margin-bottom: 25px; }
.selection-heading h2 { font-size: 19px; }
+.panel-view {
+ height: 100%;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+.panel-view-scroll {
+ overflow-y: auto;
+ padding-right: 4px;
+}
+.panel-back {
+ width: fit-content;
+ border: 0;
+ background: transparent;
+ color: var(--muted);
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 0 16px;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 720;
+}
+.panel-back:hover { color: var(--text); }
+.selected-summary {
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ background: var(--surface);
+ padding: 14px;
+ display: grid;
+ gap: 5px;
+ margin-bottom: 28px;
+}
+.selected-summary span {
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.35;
+}
+.selected-summary strong {
+ color: var(--text);
+ font-size: 24px;
+ letter-spacing: -0.04em;
+}
+.duration-picker-view {
+ margin-top: auto;
+ margin-bottom: auto;
+}
.empty-state {
min-height: 100%;
display: flex;
@@ -410,6 +482,13 @@ a { color: inherit; text-decoration: none; }
.loading-row, .no-slots { color: var(--muted); font-size: 13px; display: flex; gap: 9px; align-items: center; padding: 18px 0; }
.loading-row svg { width: 16px; }
.time-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
+.time-list {
+ min-height: 0;
+ flex: 1;
+ overflow-y: auto;
+ align-content: start;
+ padding-right: 4px;
+}
.time-button {
border: 1px solid var(--border);
background: var(--surface);
@@ -439,11 +518,8 @@ a { color: inherit; text-decoration: none; }
.duration-button.active { background: var(--text); color: var(--bg); border-color: var(--text); }
.duration-button.active span { color: color-mix(in srgb, var(--bg) 65%, transparent); }
-.details-drawer { display: none; }
-.inline-details {
- border-top: 1px solid var(--border);
- margin-top: 28px;
- padding-top: 24px;
+.compact-details {
+ gap: 11px;
animation: drawer-in 180ms ease-out;
}
@keyframes drawer-in { from { opacity: 0; transform: translateY(10px); } }
@@ -452,6 +528,11 @@ a { color: inherit; text-decoration: none; }
.drawer-summary p:last-child { color: var(--muted); font-size: 13px; }
.details-form { display: grid; gap: 16px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
+.selection-panel .form-row { grid-template-columns: 1fr; gap: 10px; }
+.selection-panel textarea { min-height: 72px; max-height: 92px; }
+.selection-panel .mode-options { grid-template-columns: 1fr; }
+.selection-panel .mode-option { padding: 9px 10px; }
+.selection-panel .primary-button { width: 100%; }
label { color: var(--muted); font-size: 11px; font-weight: 650; display: grid; gap: 7px; }
input, textarea, select {
width: 100%;
@@ -519,7 +600,16 @@ input:focus, textarea:focus, select:focus { border-color: color-mix(in srgb, var
}
.error-banner, .floating-error { color: var(--danger); background: color-mix(in srgb, var(--danger) 9%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 22%, transparent); }
.success-banner { color: var(--success); background: color-mix(in srgb, var(--success) 8%, transparent); border: 1px solid color-mix(in srgb, var(--success) 22%, transparent); margin-bottom: 14px; }
-.floating-error { width: 100%; max-width: 1320px; margin: 0; }
+.floating-error {
+ position: fixed;
+ left: 50%;
+ bottom: 56px;
+ transform: translateX(-50%);
+ width: min(1180px, calc(100vw - 32px));
+ max-width: calc(100vw - 32px);
+ margin: 0;
+ z-index: 30;
+}
.booking-page footer {
position: fixed;
left: 50%;
@@ -575,6 +665,16 @@ input:focus, textarea:focus, select:focus { border-color: color-mix(in srgb, var
.confirmation-card h1, .admin-login-card h1 { font-size: 39px; }
.confirmation-time { font-weight: 680; font-size: 14px; margin: 2px 0 0; }
.muted, .admin-login-card p { color: var(--muted); font-size: 13px; line-height: 1.55; margin: 0; }
+.confirmation-panel {
+ justify-content: center;
+ align-items: flex-start;
+ gap: 14px;
+}
+.confirmation-panel h2 {
+ margin: 0;
+ font-size: 28px;
+ letter-spacing: -0.04em;
+}
.admin-page { min-height: 100vh; padding: 22px 26px 40px; }
.admin-topbar { max-width: 1380px; margin-bottom: 24px; justify-content: flex-end; }
@@ -803,21 +903,33 @@ select { width: 100%; color: var(--text); background: var(--surface-soft); borde
}
@media (max-width: 850px) {
+ body {
+ overflow: auto;
+ }
.booking-page {
justify-content: flex-start;
align-items: stretch;
+ width: 100%;
+ height: auto;
min-height: 100vh;
+ overflow: visible;
padding: 70px 14px 42px;
}
.admin-page { padding: 14px; }
.topbar { top: 14px; left: 14px; right: 14px; width: calc(100% - 28px); margin: 0; }
- .booking-shell { display: block; min-height: 0; border-radius: 20px; }
+ .booking-shell {
+ display: block;
+ width: 100%;
+ height: auto;
+ max-height: none;
+ min-height: 0;
+ border-radius: 20px;
+ }
.booking-shell > * + * { border-left: 0; border-top: 1px solid var(--border); }
.intro-panel { min-height: 290px; padding: 30px 24px; }
.calendar-panel, .selection-panel { padding: 28px 22px; }
.day { min-height: 47px; }
.time-grid { grid-template-columns: repeat(4, 1fr); }
- .details-drawer { grid-template-columns: 1fr; gap: 14px; padding: 22px; }
.admin-layout { grid-template-columns: 1fr; }
.admin-nav { position: static; flex-direction: row; overflow-x: auto; padding: 0; }
.settings-grid { grid-template-columns: 1fr; }
@@ -849,11 +961,3 @@ select { width: 100%; color: var(--text); background: var(--surface-soft); borde
.theme-preview { width: 72px; height: 60px; }
.theme-preview span { width: 36px; height: 30px; }
}
-
-@media (min-width: 851px) and (max-height: 840px) {
- .booking-page {
- justify-content: flex-start;
- padding-top: 86px;
- padding-bottom: 54px;
- }
-}