* { box-sizing: border-box; } html, body, #root { height: 100%; } html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } body { margin: 0; position: fixed; inset: 0; overflow: hidden; overscroll-behavior: none; background: var(--shell); color: var(--ink); font-family: var(--font-ui); font-size: var(--t-3); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } ::selection { background: var(--selection); } /* A webview reads every tap as the start of a text selection: a grey flash over the button, a callout on a long press, and a caret in whatever it decided you meant. None of that belongs on chrome. All of it belongs in a field, so the fields ask for it back. */ html { -webkit-tap-highlight-color: transparent; } :root[data-touch] body { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } :root[data-touch] input, :root[data-touch] textarea, :root[data-touch] [contenteditable] { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; } /* iOS Safari zooms the page in on a focused field under 16px, and the viewport is locked, so there is no gesture that undoes it. */ :root[data-phone] input, :root[data-phone] textarea, :root[data-phone] select { font-size: var(--t-5); } button { font-family: inherit; color: inherit; border: 0; background: none; cursor: pointer; padding: 0; } input, textarea, select { font-family: inherit; font-size: inherit; color: inherit; } :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } /* The grid owns the window: one header row, no permanent sidebar, nothing else resident. */ .app { display: flex; flex-direction: column; /* Fill whatever the layout viewport turns out to be, rather than naming a unit that claims to know it. html, body and #root are already exactly that box, so this inherits the right answer on every platform without being told. Deliberately not a viewport unit. UIKit shrinks that box by the safe areas while leaving it anchored at y 0, and `stop_uikit_shrinking_the_viewport` in lib.rs is the only thing stopping it. Should that ever regress, a unit naming the whole screen would lay the tab bar out below the clip `body { position: fixed }` imposes, where it is not merely misplaced but invisible and untappable. `100%` cannot drift that way. */ height: 100%; overflow: hidden; } .titlebar { flex: none; position: relative; z-index: 45; /* Out of the way of a status bar or a notch, the same way the phone bars do it. A desktop reads both insets as zero; an iPad gets this header rather than those bars and does not. */ height: calc(var(--titlebar-h) + var(--safe-top)); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: var(--safe-top) 14px 0; background: var(--shell); border-bottom: 1px solid var(--line); /* Tauri's drag region lets the second click of a double click through to the webview so it can zoom on mouseup, and WebKit would otherwise read that click as "select the nearest word". */ -webkit-user-select: none; user-select: none; } /* The macOS traffic lights float over this row, so it opens a lane for them and costs no extra height. macOS is the only platform with any: they come from `titleBarStyle: "Overlay"`, which is a macOS-only window setting, and on Linux, Windows, an iPad or a browser the lane was 84px of nothing, pushing the view switcher off centre and starving the date range of the room it needed to say what day it is. */ :root[data-traffic] .titlebar { padding-left: var(--traffic-pad); } .titlebar .lead { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: hidden; } .titlebar .trail { display: flex; align-items: center; justify-content: flex-end; gap: 8px; } .icon-button { display: grid; place-items: center; width: 28px; height: 28px; border-radius: var(--r-sm); color: var(--ink-soft); transition: background 120ms var(--ease), color 120ms var(--ease); } .icon-button:hover { background: var(--accent-wash); color: var(--ink); } .icon-button[data-active="true"] { background: var(--accent-wash); color: var(--ink); } /* 28px is a pointer's target. A finger is nearer 44, and a tablet gets the desktop layout with none of the desktop's aim. */ :root[data-touch] .icon-button { width: var(--touch-h); height: var(--touch-h); } /* Phone chrome ---------------------------------------------------------------------------------------------- The header's three groups do not fit a 390px row, so they split: where you are goes to the top, what you are looking at goes to the bottom where the thumb already is, and the rest goes behind the overflow. Both bars are sized for a finger whether or not the pointer says it is one, since a phone-width window is a phone-shaped layout either way. */ .phonebar { flex: none; position: relative; z-index: 45; height: calc(var(--phonebar-h) + var(--safe-top)); padding: var(--safe-top) 6px 0; display: flex; align-items: center; gap: 2px; background: var(--shell); border-bottom: 1px solid var(--line); } .phonebar .icon-button { flex: none; width: var(--touch-h); height: var(--touch-h); color: var(--ink); } /* Takes what the fixed controls leave and trims the range rather than pushing them off the edge. */ .phonebar-range { flex: 1; min-width: 0; height: var(--touch-h); padding: 0 6px; border-radius: var(--r-sm); font-size: var(--t-4); font-weight: 500; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .phonebar-today { flex: none; min-width: var(--touch-h); height: var(--touch-h); padding: 0 8px; border-radius: var(--r-sm); color: var(--ink-soft); white-space: nowrap; } /* There is no hover to light up, so the press itself is the feedback. */ .phonebar-range:active, .phonebar-today:active, .tabbar-view:active { background: var(--accent-wash); } .tabbar { flex: none; position: relative; z-index: 45; height: calc(var(--tabbar-h) + var(--safe-bottom)); padding: 0 10px var(--safe-bottom); display: flex; align-items: center; gap: 10px; background: var(--shell); border-top: 1px solid var(--line); } .tabbar-views { flex: 1; min-width: 0; display: flex; gap: 2px; padding: 2px; border-radius: var(--r-md); background: var(--accent-wash); } .tabbar-view { flex: 1; min-width: 0; height: var(--touch-h); border-radius: var(--r-sm); font-size: var(--t-3); color: var(--ink-soft); transition: background 120ms var(--ease), color 120ms var(--ease); } .tabbar-view[data-active="true"] { background: var(--paper); color: var(--ink); } /* The only action with a permanent button of its own. Everything else is a row in the overflow. */ .tabbar-new { flex: none; display: grid; place-items: center; width: var(--touch-h); height: var(--touch-h); border-radius: var(--r-pill); background: var(--accent); color: var(--accent-contrast); transition: background 120ms var(--ease); } .tabbar-new:active { background: var(--accent-ink); } .stage { flex: 1; min-height: 0; display: flex; flex-direction: column; /* The first-run note lays itself over whatever is here, so this is the box it covers. */ position: relative; background: var(--paper); } /* Above every part of the grid, which stacks up to 15, and under the overlays, which start at 20: the panel this opens has to come up in front of it. */ .first-run { position: absolute; inset: 0; z-index: 19; display: grid; place-items: center; padding: 24px; background: var(--paper); } .first-run-text { display: flex; flex-direction: column; align-items: center; gap: 10px; max-width: 320px; text-align: center; } .first-run-title { margin: 0; font-family: var(--font-heading); font-weight: 500; font-size: 18px; } .first-run-note { margin: 0; color: var(--ink-soft); } /* Overlays, margin's idiom: summoned by a key, dismissed with Escape, never resident. */ .overlay { position: fixed; inset: 0; z-index: 20; background: var(--scrim); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); display: grid; place-items: center; padding: 40px; } .panel { width: min(480px, calc(100vw - 32px)); max-height: 100%; display: flex; flex-direction: column; background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-pop); overflow: hidden; } /* On a phone the same panel docks to the bottom edge: a centred box with a 40px moat around it spends the only screen there is on scrim, and puts its buttons at the far end of the reach. */ :root[data-phone] .overlay { /* Over the bars rather than under them. The desktop leaves its titlebar lit above the scrim because the traffic lights live there, but a sheet on the bottom edge would otherwise come up underneath the tab bar and lose its last row to it. */ z-index: 50; padding: 0; place-items: end center; } /* Search is the one overlay child that is not a `.panel`: it brings its own surface and its own width, so all it wants from the phone is to be clear of the home indicator. */ :root[data-phone] .overlay > :not(.panel) { margin-bottom: var(--safe-bottom); } :root[data-phone] .panel { width: 100%; max-height: var(--sheet-max-h); border-width: 1px 0 0; border-radius: var(--r-lg) var(--r-lg) 0 0; /* The home indicator sits over the foot of the sheet, so the last row stops above it. */ padding-bottom: var(--safe-bottom); animation: sheet-up 180ms var(--ease); } /* A list that has run out of rows must not start scrolling the page underneath it. */ :root[data-phone] .panel-body { overscroll-behavior: contain; } @keyframes sheet-up { from { transform: translateY(100%); } } .panel-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 14px 16px 22px; border-bottom: 1px solid var(--line); } .panel-head h2 { margin: 0; font-family: var(--font-heading); font-weight: 500; font-size: 18px; } .panel-body { display: flex; flex-direction: column; gap: 15px; padding: 20px 22px 24px; overflow-y: auto; } .panel-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line); } .field { display: flex; flex-direction: column; gap: 6px; } .field-label { font-size: var(--t-1); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); } .toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 60; max-width: min(560px, calc(100vw - 48px)); padding: 10px 16px; background: var(--glass); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border: 1px solid var(--line); border-radius: var(--r-md); box-shadow: var(--shadow-pop); color: var(--ink); white-space: pre-wrap; } /* Header. The grid owns the window, so this row is the only persistent chrome there is. */ .header-today, .header-range { height: 26px; padding: 0 9px; border-radius: var(--r-sm); color: var(--ink-soft); white-space: nowrap; transition: background 120ms var(--ease), color 120ms var(--ease); } .header-today:hover, .header-range:hover { background: var(--accent-wash); color: var(--ink); } /* At the minimum window size the agenda's range is long enough to overrun its 1fr column and paint under the view switcher, so it ellipsizes rather than collides. */ .header-range { font-size: var(--t-4); color: var(--ink); font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; } .view-switch { display: flex; gap: 2px; padding: 2px; border-radius: var(--r-md); background: var(--accent-wash); } .view-option { height: 22px; padding: 0 11px; border-radius: var(--r-sm); font-size: var(--t-2); color: var(--ink-soft); transition: background 120ms var(--ease), color 120ms var(--ease); } .view-option:hover { color: var(--ink); } .view-option[data-active="true"] { background: var(--paper); color: var(--ink); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); } .header-hours { font-size: var(--t-1); letter-spacing: 0.04em; color: var(--ink-faint); white-space: nowrap; } :root[data-compact] .header-hours { display: none; }