NobGit
public imalexnord read

Slot

Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.

Languages

Repository composition by tracked source files.

TypeScript
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
Alex Nord <[email protected]> 3 days ago
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;
-  }
-}