From cde03de0f0cab48fe4dd55649787dc6271375647 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Thu, 16 Jul 2026 05:15:00 -0700 Subject: [PATCH] Fold the mobile header into a single hamburger menu (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On phones the header spanned two rows: the title block plus a control bar of the °F/°C toggle, bell, account and hamburger. Wrap the nav in a .nav-panel inside the
menu and inject the °F/°C toggle and account/bell into that panel instead of into the header row. On desktop the panel is display:contents, so the inline top bar renders exactly as before (order restored via flex order); on phones the panel becomes a right-anchored, viewport-clamped dropdown and the header collapses to a single row (logo + name + hamburger, tagline hidden). The viewport clamp also fixes the SEO/city pages, whose menu dropdown previously ran off-screen. --- static/account.js | 8 ++- static/calendar.html | 2 + static/compare.html | 2 + static/day.html | 2 + static/index.html | 2 + static/legend.html | 2 + static/style.css | 105 +++++++++++++++++++++++++------------- static/subscriptions.html | 2 + static/units.js | 8 ++- 9 files changed, 94 insertions(+), 39 deletions(-) diff --git a/static/account.js b/static/account.js index 23fd273..8e42d03 100644 --- a/static/account.js +++ b/static/account.js @@ -259,8 +259,12 @@ function ensureAcctEl() { if (!acctEl) { acctEl = document.createElement("div"); acctEl.className = "acct"; - const anchor = brand.querySelector(".nav-menu") || brand.querySelector(".view-nav"); - brand.insertBefore(acctEl, anchor); // top-right, just left of the nav menu + // The account/bell live inside the nav menu panel: on desktop the panel is + // display:contents so they render inline top-right; on phones they stack + // inside the hamburger dropdown. Fall back to a brand sibling if unwrapped. + const panel = brand.querySelector(".nav-panel"); + if (panel) panel.appendChild(acctEl); + else brand.insertBefore(acctEl, brand.querySelector(".view-nav")); } return acctEl; } diff --git a/static/calendar.html b/static/calendar.html index a1b4ad8..aa884fa 100644 --- a/static/calendar.html +++ b/static/calendar.html @@ -35,6 +35,7 @@ diff --git a/static/compare.html b/static/compare.html index 133b871..ce2c6c5 100644 --- a/static/compare.html +++ b/static/compare.html @@ -36,6 +36,7 @@ diff --git a/static/day.html b/static/day.html index 4a7526e..1eb7c11 100644 --- a/static/day.html +++ b/static/day.html @@ -36,6 +36,7 @@ diff --git a/static/index.html b/static/index.html index 08ee6a7..7b42da3 100644 --- a/static/index.html +++ b/static/index.html @@ -39,6 +39,7 @@ diff --git a/static/legend.html b/static/legend.html index 3eb970a..6e6ebea 100644 --- a/static/legend.html +++ b/static/legend.html @@ -35,6 +35,7 @@ diff --git a/static/style.css b/static/style.css index c5d3619..4473e74 100644 --- a/static/style.css +++ b/static/style.css @@ -491,15 +491,23 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } .view-nav a:hover { color: var(--text); } .view-nav a.active { background: var(--accent); color: #fff; } -/* Hamburger wrapper. On desktop it's transparent (display:contents) so the nav - renders inline as pills and the hamburger summary is hidden; on phones (the - 640px block) it becomes a dropdown while the F/C toggle, bell and account stay - in the header bar. */ +/* Hamburger wrapper. On desktop the menu and its panel are transparent + (display:contents), so the nav pills plus the injected °F/°C toggle, bell and + account render inline in the header row and the hamburger summary is hidden; + on phones (the 640px block) the panel becomes a dropdown behind the hamburger. */ .nav-menu { display: contents; } -/* A closed
hides its non-summary content; force the nav visible on +.nav-panel { display: contents; } +/* A closed
hides its non-summary content; force the panel visible on desktop (the 640px block re-hides it behind the hamburger on phones). */ -.nav-menu > .view-nav { display: inline-flex; } +.nav-menu > .view-nav, +.nav-panel > .view-nav { display: inline-flex; } .nav-menu::details-content { display: contents; } +/* The controls are injected into the panel after the nav in DOM order; restore + the header's visual order — °F/°C · account · tabs — on desktop. On phones the + 640px block resets these so the dropdown stacks nav → °F/°C → account. */ +.nav-panel > .unit-toggle { order: 1; } +.nav-panel > .acct { order: 2; } +.nav-panel > .view-nav { order: 3; } .nav-toggle { display: none; list-style: none; cursor: pointer; } .nav-toggle::-webkit-details-marker { display: none; } .nav-toggle svg { display: block; } @@ -1308,43 +1316,70 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } header { padding: 14px 16px; } h1 { font-size: 19px; } - .tag { font-size: 12px; } + /* Drop the tagline on phones so the header stays a tight single row. */ + .tag { display: none; } - /* The title (logo + name + tagline) claims the whole first row so its text never - collapses to a one-word-per-line sliver; the F/C toggle, bell, account and - hamburger wrap to a control bar on the row below. (A lone hamburger — the SEO - pages, which have no toggle/account — is small enough to stay on the title row.) */ - .brand { flex-wrap: wrap; align-items: flex-start; row-gap: 12px; } - .brand > div:not(.unit-toggle):not(.acct) { flex: 1 1 0; min-width: calc(100% - 92px); } - .logo { align-self: flex-start; margin-top: 3px; } - /* The view navigation collapses into a hamburger dropdown; the F/C toggle, bell - and account sit in the control bar beside it. */ - /* A real box again (not display:contents) so it's the positioning context for - the absolute dropdown and holds the hamburger. */ - .nav-menu { display: inline-flex; position: relative; align-self: center; } + /* One compact row: logo + name on the left, the hamburger flush-right. The nav, + °F/°C toggle, bell and account all fold into the hamburger dropdown below, so + the header never grows a second control-bar row. */ + .brand { flex-wrap: nowrap; align-items: center; gap: 10px; } + .brand > div:not(.unit-toggle):not(.acct) { flex: 1 1 auto; min-width: 0; } + .logo { align-self: center; margin-top: 0; } + + /* A real box again (not display:contents) so it anchors the absolute dropdown + and holds the hamburger, pinned to the header's right edge. */ + .nav-menu { display: inline-flex; position: relative; align-self: center; flex: 0 0 auto; } .nav-toggle { display: inline-flex; align-items: center; justify-content: center; - width: 44px; height: 40px; border: 1px solid var(--border); border-radius: 10px; + width: 44px; height: 44px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); color: var(--text); } .nav-menu[open] .nav-toggle { border-color: var(--accent); color: var(--accent); } - .view-nav { - position: absolute; right: 0; top: calc(100% + 8px); z-index: 60; margin: 0; - flex-direction: column; align-items: stretch; width: max-content; min-width: 176px; - box-shadow: 0 12px 30px rgba(0, 0, 0, .35); + + /* The panel is the dropdown: right-anchored and clamped to the viewport so it + never runs off-screen (this also fixes the SEO/city pages, whose only panel + item is the nav). Contents stack nav → °F/°C → account top-to-bottom. */ + .nav-panel { + display: flex; flex-direction: column; align-items: stretch; gap: 8px; + position: absolute; right: 0; top: calc(100% + 8px); z-index: 60; + margin: 0; padding: 10px; width: max-content; min-width: 224px; + max-width: calc(100vw - 28px); + background: var(--surface); border: 1px solid var(--border); border-radius: 14px; + box-shadow: 0 16px 40px rgba(0, 0, 0, .45); } - .nav-menu:not([open]) .view-nav { display: none; } - .view-nav a { min-height: 44px; padding: 10px 14px; justify-content: flex-start; } + .nav-menu:not([open]) .nav-panel { display: none; } + /* Reset the desktop cluster order — the dropdown wants plain DOM order. */ + .nav-panel > .unit-toggle, .nav-panel > .acct, .nav-panel > .view-nav { order: 0; } + + /* Nav links: a full-width vertical list, hairline-separated from the controls + below (the :not(:last-child) keeps SEO pages — nav only — divider-free). */ + .view-nav { + display: flex; flex-direction: column; align-items: stretch; gap: 2px; margin: 0; + background: transparent; border: 0; border-radius: 0; padding: 0; + } + .nav-panel > .view-nav:not(:last-child) { + padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border); + } + .view-nav a { min-height: 44px; padding: 11px 12px; justify-content: flex-start; border-radius: 9px; } .view-nav a + a::before { display: none; } - /* Collapse the account entry to an icon-only button so it doesn't crush the - title/tagline on narrow screens (it still opens the modal / menu). */ - .acct-btn span { display: none; } - .acct-btn { padding: 8px; min-width: 40px; justify-content: center; } - /* The bell sits left of the account button, so a wide dropdown anchored to the - bell (.notif) overflowed off the left of the screen. Drop .notif's positioning - context so the dropdown anchors to the .acct cluster at the header's right - edge instead, keeping it fully on-screen. */ - .notif { position: static; } + + /* °F/°C toggle: full-width segmented control inside the dropdown. */ + .nav-panel > .unit-toggle { margin: 0; width: 100%; align-self: stretch; } + .nav-panel > .unit-toggle button { flex: 1; } + + /* Account row (bell + account/Sign in) spans the dropdown; the account button + keeps its label and fills, the bell stays a fixed square. Popovers open below, + clamped to the viewport so the notification list never overflows. */ + .nav-panel > .acct { margin: 0; width: 100%; align-self: stretch; } + .nav-panel > .acct .acct-menu { flex: 1; } + .nav-panel > .acct .acct-btn:not(.notif-btn) { width: 100%; justify-content: center; } + /* The bell's notification list would anchor to the small bell button and spill + off the left edge; drop .notif's positioning context so it anchors to the + panel instead — dropping below the menu, right-aligned and within the + viewport. The account menu already fits; just clamp its width. */ + .nav-panel .notif { position: static; } + .nav-panel .notif-pop { right: 0; width: min(340px, calc(100vw - 24px)); } + .acct-pop { max-width: calc(100vw - 40px); } main { padding: 14px 14px 40px; } .controls { gap: 12px; margin-bottom: 14px; } diff --git a/static/subscriptions.html b/static/subscriptions.html index 18c7e2d..7290323 100644 --- a/static/subscriptions.html +++ b/static/subscriptions.html @@ -33,6 +33,7 @@ diff --git a/static/units.js b/static/units.js index 3dd3518..8a81e68 100644 --- a/static/units.js +++ b/static/units.js @@ -63,7 +63,11 @@ function syncUnitToggle(wrap) { const b = e.target.closest("button[data-unit]"); if (b) setUnit(b.dataset.unit); }); - const anchor = brand.querySelector(".nav-menu") || brand.querySelector(".view-nav"); - brand.insertBefore(wrap, anchor); // sits just left of the nav (top-right) + // Lives inside the nav menu panel: display:contents keeps it inline top-right + // on desktop; on phones it stacks inside the hamburger dropdown. Fall back to a + // brand sibling if the panel isn't present. + const panel = brand.querySelector(".nav-panel"); + if (panel) panel.appendChild(wrap); + else brand.insertBefore(wrap, brand.querySelector(".view-nav")); syncUnitToggle(wrap); })();