Spaces:
Sleeping
Sleeping
Download client/app.css from lvwerra/cowrite-dev: direct link, hf CLI and curl.
- Browser
- Download file 105 kB
-
https://huggingface.co/spaces/lvwerra/cowrite-dev/resolve/main/client/app.css
- Command line
-
hf download hf://spaces/lvwerra/cowrite-dev/client/app.css
-
curl -L -o app.css https://huggingface.co/spaces/lvwerra/cowrite-dev/resolve/main/client/app.css
105 kB
| /* Inter, served from this origin. It used to come from fonts.googleapis.com, | |
| which meant two extra TLS handshakes and a render-blocking stylesheet on a | |
| third party before the page could paint anything — including the skeleton. | |
| Latin + latin-ext only; anything else falls back down the stack in --ui. */ | |
| @font-face { | |
| font-family: 'Inter'; | |
| font-style: normal; | |
| font-weight: 400 700; | |
| font-display: swap; | |
| src: url('/fonts/inter-latin-ext.woff2') format('woff2'); | |
| unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| } | |
| @font-face { | |
| font-family: 'Inter'; | |
| font-style: normal; | |
| font-weight: 400 700; | |
| font-display: swap; | |
| src: url('/fonts/inter-latin.woff2') format('woff2'); | |
| unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| } | |
| /* Libre Baskerville, for the "Reading" document style. Four files, 132KB, | |
| each well inside the ~100KB the Hub allows a non-LFS binary — and each a | |
| variable font carrying wght 400-700, so bold and bold italic are interpolated | |
| from the real design rather than synthesised by slanting or smearing a roman. | |
| (Google serves one file per style for both weights; the pair of weights in | |
| the descriptor below is what tells the browser to use the axis.) | |
| Chosen by eye from a tuner over fourteen serifs, not by distance from the | |
| magazine's own Ubique Text — that face was cut for the Council on Foreign | |
| Relations and cannot be licensed, so the comparison had a floor. What the | |
| measurements did settle is the sizing below. | |
| Nothing references them until data-doc-style="reading" is stamped, and an | |
| unused @font-face is never fetched — Docs-style readers pay nothing. */ | |
| @font-face { | |
| font-family: 'Libre Baskerville'; | |
| font-style: normal; | |
| font-weight: 400 700; | |
| font-display: swap; | |
| src: url('/fonts/libre-baskerville-latin-ext.woff2') format('woff2'); | |
| unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| } | |
| @font-face { | |
| font-family: 'Libre Baskerville'; | |
| font-style: italic; | |
| font-weight: 400 700; | |
| font-display: swap; | |
| src: url('/fonts/libre-baskerville-latin-ext-italic.woff2') format('woff2'); | |
| unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| } | |
| @font-face { | |
| font-family: 'Libre Baskerville'; | |
| font-style: normal; | |
| font-weight: 400 700; | |
| font-display: swap; | |
| src: url('/fonts/libre-baskerville-latin.woff2') format('woff2'); | |
| unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| } | |
| @font-face { | |
| font-family: 'Libre Baskerville'; | |
| font-style: italic; | |
| font-weight: 400 700; | |
| font-display: swap; | |
| src: url('/fonts/libre-baskerville-latin-italic.woff2') format('woff2'); | |
| unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| } | |
| * { box-sizing: border-box; } | |
| :root { | |
| color-scheme: light; | |
| --bg: #f9fbfd; /* Docs' canvas behind the page */ | |
| --surface: #ffffff; | |
| --border: #e7e7ea; | |
| --border-strong: #d9d9de; | |
| --text: #1a1a1e; | |
| --text-2: #55555e; | |
| --text-3: #9d9da6; | |
| --accent: #1d5c45; | |
| --accent-dark: #144533; | |
| --accent-soft: #e9f1ec; | |
| --hl: rgba(29, 92, 69, 0.1); | |
| --hl-active: rgba(29, 92, 69, 0.22); | |
| --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05); | |
| --shadow-md: 0 4px 16px rgba(20, 20, 30, 0.08), 0 1px 3px rgba(20, 20, 30, 0.06); | |
| /* --line and --paper were used in 14 places without ever being defined, so | |
| those borders silently never rendered */ | |
| --line: var(--border); | |
| --paper: var(--surface); | |
| --page: #ffffff; /* the document sheet */ | |
| --page-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15); | |
| --input-bg: #ffffff; | |
| --tint: rgba(0, 0, 0, 0.05); /* neutral hover/fill over a surface */ | |
| --tint-strong: rgba(0, 0, 0, 0.12); | |
| --hl-weak: rgba(29, 92, 69, 0.06); /* accent washes: nav hover, strips, bars */ | |
| --hl-edge: rgba(29, 92, 69, 0.55); /* the rule beside quoted/commented text */ | |
| --find: #ffe08a; /* search hits — amber, not accent: see mark.search-hl */ | |
| --scrim: rgba(18, 18, 24, 0.4); | |
| --danger: #b03030; | |
| --chip-bg: #f0f0f3; | |
| --menu-solid: var(--text); /* the dark selection bubble */ | |
| --tooltip-bg: #223029; | |
| --on-primary: #ffffff; /* text on a filled primary button */ | |
| --primary-hover: #000000; | |
| --ins-active: #cfe5d9; /* suggestion insertions, focused */ | |
| --ins-block-active: #d9ebe1; | |
| --del-bg: rgba(0, 0, 0, 0.045); /* suggestion deletions */ | |
| --del-bg-active: rgba(0, 0, 0, 0.1); | |
| --sugg-bar: #99b6ab; /* the range bar in the page margin */ | |
| --doc-text: #000000; /* the document's own ink */ | |
| --doc-text-2: #434343; | |
| --doc-text-3: #666666; | |
| --doc-rule: #d0d0d0; /* dividers, blockquote bar */ | |
| --table-rule: #000000; /* Docs draws table cells in black */ | |
| --code-bg: #f1f3f4; | |
| --code-block-bg: #f8f9fa; | |
| --code-block-border: #e0e3e7; | |
| --ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| /* the document itself is Google Docs' default: Arial 11pt. Liberation Sans and | |
| Arimo are the metric-compatible stand-ins on Linux. */ | |
| --doc: Arial, "Liberation Sans", Arimo, Helvetica, sans-serif; | |
| --doc-size: 14.6667px; /* 11pt at 96dpi */ | |
| --doc-line: 1.15; /* Docs' default line spacing for Normal text */ | |
| /* Reader text size — the Kindle control, and deliberately the opposite of the | |
| zoom below. It multiplies the type and NOTHING else, so the sheet keeps the | |
| width it has on screen and the lines re-break. Measured over five prose | |
| paragraphs in this 624px column, Docs runs 96 characters a line at 80% and | |
| 56 at 160%; Reading runs 79 down to 42. The band typographers settle on is | |
| 60-75, which Docs' own default (83) sits just outside and Reading's (67) | |
| sits inside — so a step or two up in Docs is a real reading improvement, | |
| and 120% of Arial lands within a few characters of the measure Reading | |
| gives for free. Reflowing is the entire point, and it is exactly what zoom | |
| must never do. | |
| A multiplier on top of --doc-size rather than a size of its own, because | |
| the two document styles set different bases (Docs 14.6667px, Reading 16px) | |
| and every heading, list indent and rule in both is sized in em off that | |
| base. Storing an absolute "18px" would mean a reader who went one step up | |
| in Docs and then switched to Reading either landed back at Reading's | |
| default (the setting silently lost) or dragged Docs' proportions into | |
| Reading (the style silently overruled). A factor rides along instead: one | |
| step up stays one step up through the switch, Reading's 3.41em headline and | |
| its small-caps section heads keep their measured proportion to the text | |
| they head, and each style still decides what 100% means for it. */ | |
| --doc-text-scale: 1; | |
| --page-w: 8.5in; /* US Letter, like a default Docs page */ | |
| --page-pad: 1in; /* Docs' default margins */ | |
| /* Zoom magnifies the *page*, not just its type: the sheet, its margins and | |
| its height all take the same factor as the font, so 80% really does make | |
| the document smaller and 150% overflows into a horizontal scroll. Scaling | |
| every length by one factor is also what keeps the line breaks identical at | |
| every zoom — a page that reflowed would not be a zoom. */ | |
| --sheet-w: calc(var(--page-w) * var(--doc-zoom, 1)); | |
| --sheet-pad: calc(var(--page-pad) * var(--doc-zoom, 1)); | |
| --sheet-min-h: calc(11in * var(--doc-zoom, 1)); | |
| --layout-edge: 20px; | |
| --layout-gap: 20px; | |
| --sidebar-w: 220px; /* minimum Pages width, read back by fitLayout() */ | |
| --margin-w: 300px; | |
| --comment-text-overlap: 24px; | |
| --margin-applied-overlap: 0px; | |
| --canvas: #f9fbfd; /* the grey-blue surround behind the page */ | |
| --doc-link: #1155cc; /* Docs' link blue */ | |
| --mono: "Courier New", Courier, ui-monospace, monospace; | |
| } | |
| /* min-height, NOT height: with height:100% the body box is exactly one viewport | |
| tall, which is also the sticky containing block — the header would then scroll | |
| away for good after the first screenful. */ | |
| html, body { margin: 0; min-height: 100%; } | |
| html { -webkit-text-size-adjust: 100%; } | |
| /* NB: never put overflow-x on html/body — it turns the body into its own | |
| scrollport and breaks the sticky header again. Runaway width is clipped on | |
| the layout containers instead. */ | |
| body { font: 14px/1.55 var(--ui); color: var(--text); background: var(--bg); } | |
| button { font: inherit; color: inherit; } | |
| svg.icon { width: 15px; height: 15px; stroke: currentColor; stroke-width: 1.7; fill: none; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; display: block; } | |
| .hidden { display: none ; } | |
| .muted { color: var(--text-3); font-size: 13px; } | |
| .spacer { flex: 1; } | |
| .row { display: flex; gap: 8px; align-items: center; } | |
| .btn { | |
| border: 1px solid var(--border-strong); background: var(--surface); color: var(--text); | |
| border-radius: 7px; padding: 6px 14px; cursor: pointer; text-decoration: none; | |
| display: inline-flex; align-items: center; gap: 6px; font-weight: 500; font-size: 13.5px; | |
| transition: border-color .12s, background .12s; | |
| } | |
| .btn:hover { border-color: var(--text-3); } | |
| .btn.primary { background: var(--text); border-color: var(--text); color: var(--on-primary); } | |
| .btn.primary:hover { background: var(--primary-hover); } | |
| .btn.accent { background: var(--accent); border-color: var(--accent); color: var(--on-primary); } | |
| .btn.accent:hover { background: var(--accent-dark); } | |
| .btn.small { padding: 4px 10px; font-size: 12.5px; } | |
| .btn.ghost { border-color: transparent; background: transparent; color: var(--text-2); } | |
| .btn.ghost:hover { background: var(--tint); color: var(--text); } | |
| .iconbtn { | |
| border: none; background: transparent; color: var(--text-2); border-radius: 6px; | |
| padding: 5px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; | |
| } | |
| .iconbtn:hover { background: var(--tint); color: var(--text); } | |
| input, textarea { | |
| font: inherit; border: 1px solid var(--border-strong); border-radius: 7px; padding: 6px 10px; | |
| width: 100%; background: var(--surface); color: var(--text); | |
| } | |
| input:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } | |
| textarea { resize: vertical; } | |
| /* --- sign-in overlay --- */ | |
| .overlay { position: fixed; inset: 0; background: var(--scrim); backdrop-filter: blur(2px); z-index: 100; display: flex; align-items: center; justify-content: center; } | |
| .overlay .card { | |
| background: var(--surface); border-radius: 14px; padding: 40px 44px; max-width: 400px; | |
| box-shadow: var(--shadow-md); text-align: center; | |
| } | |
| .overlay .card h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: -0.01em; } | |
| .overlay .card p { color: var(--text-2); margin: 0 0 22px; } | |
| #dev-login { margin-top: 14px; display: flex; gap: 8px; } | |
| /* --- header --- */ | |
| #topbar { | |
| display: flex; align-items: center; gap: 10px; padding: 0 16px; height: 52px; | |
| background: var(--surface); border-bottom: 1px solid var(--border); | |
| position: sticky; top: 0; z-index: 40; | |
| } | |
| #topbar .wordmark { font-weight: 650; letter-spacing: -0.02em; text-decoration: none; color: var(--text); font-size: 15px; } | |
| #back-link { color: var(--text-2); text-decoration: none; display: flex; align-items: center; padding: 6px; border-radius: 6px; } | |
| #back-link:hover { background: var(--tint); color: var(--text); } | |
| #doc-title { font-weight: 600; font-size: 14px; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 260px; } | |
| #conn { width: 7px; height: 7px; border-radius: 50%; background: var(--text-3); flex-shrink: 0; } | |
| #conn.on { background: var(--accent); } | |
| #whoami { font-size: 13px; color: var(--text-2); display: flex; align-items: center; gap: 8px; white-space: nowrap; } | |
| #whoami img { width: 24px; height: 24px; border-radius: 50%; } | |
| #whoami a { color: var(--text-3); text-decoration: none; } | |
| #whoami a:hover { color: var(--text); } | |
| #presence { display: flex; } | |
| #presence .avatar { | |
| width: 24px; height: 24px; border-radius: 50%; color: #fff; font-size: 10px; font-weight: 600; | |
| display: flex; align-items: center; justify-content: center; border: 2px solid var(--surface); margin-left: -6px; | |
| } | |
| #presence .avatar:first-child { margin-left: 0; } | |
| /* toolbar inside header */ | |
| #header-tools { display: flex; align-items: center; gap: 0; margin: 0 4px; overflow-x: auto; scrollbar-width: none; } | |
| #header-tools::-webkit-scrollbar { display: none; } | |
| /* When the toolbar cannot fit beside the title and the actions it takes a row | |
| of its own, exactly as it does below 980px — same rules, different reason. | |
| Stamped by fitHeaderTools() from a measurement rather than by a media query, | |
| because the width the toolbar needs is not a function of the window width. | |
| #doc-title is capped at 260px but a short one is ~66px, and every collaborator | |
| present adds an 18px avatar, so the room left over on the row swings by 200px | |
| between two windows of the same size. Measured on this toolbar: with a short | |
| title it stops fitting at 1340px, with a full-length one at 1520px. A | |
| breakpoint set for either of those is wrong for the other — and the hidden | |
| scrollbar (above) means being wrong is silent, which is how the image button | |
| went missing at 1280px without anything on screen saying so. */ | |
| body.tools-own-row #topbar { flex-wrap: wrap; align-content: center; height: auto; min-height: 52px; } | |
| body.tools-own-row #header-tools { | |
| order: 10; flex-basis: 100%; width: 100%; margin: 0; | |
| padding: 3px 0 1px; border-top: 1px solid var(--border); | |
| } | |
| #header-tools button { | |
| border: none; background: transparent; border-radius: 6px; padding: 5px 6px; cursor: pointer; | |
| font-size: 12.5px; color: var(--text-2); min-width: 26px; font-weight: 500; display: inline-flex; align-items: center; justify-content: center; | |
| white-space: nowrap; flex-shrink: 0; | |
| } | |
| #header-tools button:hover { background: var(--tint); color: var(--text); } | |
| #header-tools button.on { background: var(--accent-soft); color: var(--accent); } | |
| #header-tools .sep { width: 1px; height: 18px; background: var(--border); margin: 0 3px; flex-shrink: 0; } | |
| /* link popover */ | |
| #link-pop { | |
| position: fixed; z-index: 60; background: var(--surface); border: 1px solid var(--border); | |
| border-radius: 10px; box-shadow: var(--shadow-md); padding: 10px; width: 320px; display: flex; gap: 6px; | |
| } | |
| /* document switcher (top left) */ | |
| #docs-btn { | |
| display: flex; align-items: center; gap: 5px; background: none; border: 0; cursor: pointer; | |
| padding: 3px 6px; border-radius: 7px; color: var(--text); font: inherit; max-width: 320px; | |
| } | |
| #docs-btn:hover { background: var(--hover); } | |
| #docs-caret .icon { width: 14px; height: 14px; opacity: 0.55; } | |
| #docs-pop { | |
| position: fixed; z-index: 60; left: 14px; top: 58px; width: 330px; | |
| background: var(--surface); border: 1px solid var(--border); border-radius: 12px; | |
| box-shadow: var(--shadow-md); padding: 14px 16px; | |
| } | |
| #docs-pop h3 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-3); } | |
| #docs-list { max-height: 52vh; overflow-y: auto; margin: 0 -6px; } | |
| .doc-row { display: flex; align-items: center; gap: 6px; border-radius: 8px; padding: 0 6px; } | |
| .doc-row:hover { background: var(--hover); } | |
| .doc-row.current { background: var(--accent-soft); } | |
| .doc-row-main { display: flex; flex-direction: column; gap: 1px; padding: 7px 0; flex: 1; min-width: 0; text-decoration: none; color: var(--text); } | |
| .doc-row .title { font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| /* the project's subtitle (home page's first paragraph) — a description line */ | |
| .doc-row .doc-row-sub { font-size: 12px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .doc-row .meta { font-size: 11.5px; color: var(--text-3); } | |
| .doc-row .iconbtn { opacity: 0; flex-shrink: 0; } | |
| .doc-row:hover .iconbtn, .doc-row .iconbtn:focus-visible { opacity: 1; } | |
| .docs-actions { display: flex; gap: 6px; margin-top: 12px; } | |
| .empty-doc { padding: 60px 10px; color: var(--text-2); text-align: center; font-size: 14px; } | |
| /* agents panel */ | |
| #agents-pop, #share-pop, #download-pop { | |
| position: fixed; z-index: 60; right: 14px; top: 58px; width: 340px; | |
| background: var(--surface); border: 1px solid var(--border); border-radius: 12px; | |
| box-shadow: var(--shadow-md); padding: 14px 16px; | |
| } | |
| #share-pop { right: 90px; } | |
| #download-pop { right: 160px; width: 300px; } | |
| #download-pop.hidden { display: none; } | |
| #agents-pop h3, #share-pop h3, #download-pop h3 { margin: 0 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-3); } | |
| #agents-pop .hint, #share-pop .hint, #download-pop .hint { font-size: 12.5px; color: var(--text-2); margin: 0 0 10px; } | |
| /* two segmented choices: how much, and in what shape */ | |
| .dl-choice { display: flex; gap: 0; margin: 8px 0 0; } | |
| .dl-choice .dl-opt { | |
| flex: 1; padding: 6px 8px; font: inherit; font-size: 12.5px; cursor: pointer; | |
| background: var(--surface); color: var(--text-2); | |
| border: 1px solid var(--border); border-right-width: 0; | |
| } | |
| .dl-choice .dl-opt:first-child { border-radius: 7px 0 0 7px; } | |
| .dl-choice .dl-opt:last-child { border-radius: 0 7px 7px 0; border-right-width: 1px; } | |
| .dl-choice .dl-opt:hover { background: var(--hl-weak); color: var(--text); } | |
| .dl-choice .dl-opt.on { background: var(--hl); color: var(--accent); font-weight: 500; } | |
| #download-pop .hint { margin: 10px 0; min-height: 2.6em; } | |
| #share-form { display: flex; gap: 6px; margin-top: 10px; } | |
| .share-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: 13.5px; } | |
| .share-row .iconbtn { margin-left: auto; } | |
| .key-box { | |
| margin-top: 10px; border: 1px solid var(--accent); background: var(--accent-soft); | |
| border-radius: 8px; padding: 10px 12px; font-size: 12.5px; | |
| } | |
| .key-box code { font-family: var(--mono); font-size: 11.5px; word-break: break-all; display: block; margin: 6px 0; } | |
| .key-box .row { margin-top: 6px; } | |
| #agents-pop .hint { font-size: 12.5px; color: var(--text-2); margin: 0 0 10px; } | |
| .agent-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: 13.5px; } | |
| .agent-row .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border-strong); flex-shrink: 0; } | |
| .agent-row .dot.on { background: var(--accent); } | |
| .agent-row .handle { font-weight: 600; } | |
| .agent-row .copy { margin-left: auto; } | |
| #agent-form { display: flex; gap: 6px; margin-top: 10px; } | |
| /* --- layout: editor + margin --- */ | |
| #layout { | |
| width: 100%; margin: 0; padding: 24px var(--layout-edge) 40vh; | |
| display: flex; gap: var(--layout-gap); align-items: flex-start; | |
| } | |
| /* The sheet is a page, so it keeps its width instead of being squeezed: when | |
| the window can no longer hold it, fitLayout() folds the Pages sidebar away | |
| first, then the comments, and only a window narrower than the page itself | |
| ends up scrolling sideways here. An over-wide child of a block scroller | |
| resolves its auto margins to zero, so it lands flush left and scrolls right | |
| — centred while it fits, fully reachable when it doesn't. */ | |
| #editor-col { | |
| flex: 1; min-width: 0; position: relative; | |
| overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; | |
| } | |
| /* the sheet: letter width, 1in margins, Docs' soft drop shadow, square corners */ | |
| #editor { | |
| background: var(--page); border: none; border-radius: 0; | |
| width: var(--sheet-w); margin: 0 auto; | |
| padding: var(--sheet-pad); min-height: var(--sheet-min-h); | |
| box-shadow: var(--page-shadow); | |
| } | |
| #margin-col { | |
| width: var(--margin-w); flex-shrink: 0; position: relative; z-index: 1; | |
| margin-left: calc(-1 * var(--margin-applied-overlap)); pointer-events: none; | |
| } | |
| #margin-tools, #margin-head, #margin-items > * { pointer-events: auto; } | |
| #margin-tools { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-3); margin-bottom: 14px; user-select: none; } | |
| #margin-tools input[type="checkbox"] { width: auto; accent-color: var(--accent); margin: 0; } | |
| #margin-items { position: relative; } | |
| /* mobile-only chrome: hidden on desktop, revealed in the phone media query */ | |
| .phone-only { display: none ; } | |
| #margin-head { display: none; } | |
| /* long comment/rationale bodies are clamped to a readable height with a | |
| "Show more" toggle (js adds .clamped once it measures an overflow) */ | |
| .msg .text.clamped, .rationale.clamped { max-height: 9.5em; overflow: hidden; position: relative; } | |
| .msg .text.clamped::after, .rationale.clamped::after { | |
| content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2.2em; | |
| background: linear-gradient(to bottom, rgba(255,255,255,0), var(--surface) 85%); | |
| pointer-events: none; | |
| } | |
| .more-btn { | |
| border: none; background: transparent; padding: 2px 0; margin-top: 2px; cursor: pointer; | |
| font: inherit; font-size: 12px; font-weight: 600; color: var(--accent); | |
| } | |
| .more-btn:hover { text-decoration: underline; } | |
| /* --- editor content --- */ | |
| /* Google Docs' defaults: Arial 11pt, line spacing 1.15, no space between | |
| paragraphs, and headings at regular weight (Docs does not bold them) — sizes | |
| and the padding-before/after pattern follow Docs' own exported stylesheet. | |
| The two size knobs meet on this one line and nowhere else: --doc-text-scale | |
| re-sets the type inside a page that keeps its width, and --doc-zoom then | |
| magnifies that page, type and all. */ | |
| .tiptap { outline: none; font-family: var(--doc); font-size: calc(var(--doc-size) * var(--doc-text-scale, 1) * var(--doc-zoom, 1)); line-height: var(--doc-line); color: var(--doc-text); } | |
| .tiptap > * + * { margin-top: 0; } | |
| .tiptap p { margin: 0; } | |
| .tiptap h1, .tiptap h2, .tiptap h3, .tiptap h4, .tiptap h5, .tiptap h6 { | |
| font-family: var(--doc); font-weight: 400; line-height: var(--doc-line); letter-spacing: normal; color: var(--doc-text); | |
| } | |
| .tiptap h1 { font-size: 1.818em; margin: 1.212em 0 0.364em; } /* 20pt, 20pt before / 6pt after */ | |
| .tiptap h2 { font-size: 1.455em; margin: 1.091em 0 0.364em; } /* 16pt, 18pt / 6pt */ | |
| .tiptap h3 { font-size: 1.273em; margin: 0.970em 0 0.242em; color: var(--doc-text-2); } /* 14pt, 16pt / 4pt */ | |
| .tiptap h4 { font-size: 1.091em; margin: 0.848em 0 0.242em; color: var(--doc-text-3); } | |
| .tiptap h5 { font-size: 1em; margin: 0.848em 0 0.242em; color: var(--doc-text-3); } | |
| .tiptap h6 { font-size: 1em; font-style: italic; margin: 0.848em 0 0.242em; color: var(--doc-text-3); } | |
| /* the page's opening heading reads as Docs' Title style (26pt) */ | |
| .tiptap > h1:first-child { font-size: 2.364em; margin-top: 0; margin-bottom: 0.182em; } | |
| /* Docs has no code block; keep ours but in its monospace (Courier New) */ | |
| .tiptap pre { | |
| background: var(--code-block-bg); border: 1px solid var(--code-block-border); border-radius: 2px; padding: 10px 12px; font-size: 0.95em; | |
| overflow-x: auto; font-family: var(--mono); line-height: 1.35; | |
| } | |
| .tiptap code { font-family: var(--mono); font-size: 0.95em; background: var(--code-bg); border-radius: 2px; padding: 0 0.2em; } | |
| .tiptap pre code { background: none; padding: 0; } | |
| .tiptap blockquote { border-left: 3px solid var(--doc-rule); margin: 0; padding-left: 18pt; color: inherit; font-style: normal; } | |
| .tiptap a { color: var(--doc-link); text-decoration: underline; text-decoration-color: currentColor; text-underline-offset: 1px; } | |
| /* Docs indents list text to 0.5in (36pt) with the marker sitting inside it */ | |
| .tiptap ul, .tiptap ol { padding-left: 36pt; margin: 0; } | |
| .tiptap li { margin: 0; } | |
| .tiptap li > p { margin: 0; } | |
| .tiptap hr { border: none; border-top: 1px solid var(--doc-rule); margin: 0.75em 0; } | |
| .tiptap img { max-width: 100%; border-radius: 8px; display: block; margin: 0.4em 0; } | |
| .tiptap img.ProseMirror-selectednode { outline: 3px solid var(--accent-soft); outline-offset: 2px; } | |
| /* Search hits inside the open document, live for as long as the sidebar box has | |
| a query in it. Amber, not the accent green: every other mark on the text means | |
| somebody said something about it (a comment, a suggestion, a pending | |
| selection), and a search hit means nothing of the sort — it has to read as a | |
| different kind of thing at a glance. The document's own ink is left alone, the | |
| way find-in-page does it, so a highlighted link is still visibly a link. | |
| box-decoration-break keeps a hit that wraps across a line from losing its | |
| corners mid-word. */ | |
| .tiptap mark.search-hl { | |
| background: var(--find); color: inherit; | |
| border-radius: 2px; padding: 0 1px; margin: 0 -1px; | |
| -webkit-box-decoration-break: clone; box-decoration-break: clone; | |
| } | |
| .tiptap .comment-hl { background: var(--hl); border-bottom: 1.5px solid var(--hl-edge); cursor: pointer; } | |
| .tiptap .comment-hl.active { background: var(--hl-active); } | |
| /* What the open composer is about. The browser drops its own selection | |
| highlight the moment focus moves to the box, so this stands in for it — | |
| dashed, to read as "not committed yet" next to real comment highlights. */ | |
| .tiptap .pending-hl { background: var(--hl-weak); border-bottom: 1.5px dashed var(--hl-edge); } | |
| /* A comment and a suggestion over the same words: the comment keeps its | |
| underline but drops the fill, so the suggestion's inserted and deleted spans | |
| are not two tints deep and stay legible. */ | |
| .tiptap .comment-hl.with-sugg { background: transparent; border-bottom-style: dotted; } | |
| .tiptap .comment-hl.with-sugg.active { background: transparent; border-bottom-width: 2px; } | |
| .tiptap .comment-range-active { position: relative; } | |
| .tiptap .comment-range-active::after { | |
| content: ''; position: absolute; left: -16px; top: 2px; bottom: 2px; | |
| width: 3.5px; background: var(--accent); pointer-events: none; | |
| } | |
| .tiptap .suggestion-hl { background: transparent; position: relative; } | |
| .tiptap .suggestion-hl::before { content: ''; position: absolute; left: -16px; top: 2px; bottom: 2px; width: 2.5px; background: var(--sugg-bar); } | |
| .tiptap p.is-editor-empty:first-child::before { color: var(--text-3); content: attr(data-placeholder); float: left; height: 0; pointer-events: none; } | |
| /* collaboration carets */ | |
| .collaboration-carets__caret { border-left: 1px solid; border-right: 1px solid; margin: 0 -1px; pointer-events: none; word-break: normal; position: relative; } | |
| .collaboration-carets__label { | |
| border-radius: 3px 3px 3px 0; color: #fff; font-size: 10px; font-weight: 600; font-family: var(--ui); | |
| left: -1px; line-height: normal; padding: 0.1rem 0.35rem; position: absolute; top: -1.35em; user-select: none; white-space: nowrap; | |
| } | |
| /* --- selection menu --- */ | |
| #selection-menu { position: absolute; z-index: 30; display: flex; background: var(--menu-solid); border-radius: 8px; box-shadow: var(--shadow-md); overflow: hidden; } | |
| #selection-menu button { | |
| background: transparent; border: none; color: #fff; padding: 7px 13px; cursor: pointer; | |
| display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 500; | |
| } | |
| #selection-menu button + button { border-left: 1px solid rgba(255,255,255,.15); } | |
| #selection-menu button:hover { background: rgba(255,255,255,.12); } | |
| /* --- the hover comment button --- */ | |
| /* Sits in the page's own 1in right margin (JS puts it 8px right of the text | |
| column), so revealing it never reflows a line — there is no gutter here, only | |
| a hover zone. z-index 20 is above #margin-col's 1: in the squeezed layout the | |
| comments panel overlaps the page's right margin, and a card drifting over the | |
| button must not swallow the click. | |
| 28px is the smallest square that still reads as a target next to 11pt text; | |
| below that the bubble glyph loses its tail and it looks like a smudge. */ | |
| #hover-comment { | |
| position: absolute; z-index: 20; width: 28px; height: 28px; padding: 0; | |
| display: flex; align-items: center; justify-content: center; | |
| background: var(--surface); color: var(--text-3); | |
| border: 1px solid var(--border); border-radius: 50%; cursor: pointer; | |
| box-shadow: var(--shadow-sm); | |
| /* No transition on top/left: the button hops between paragraphs as the pointer | |
| travels, and an eased hop reads as lag. Only the hover state animates. */ | |
| transition: color .12s, border-color .12s, background .12s; | |
| } | |
| #hover-comment:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); } | |
| #hover-comment .icon { width: 15px; height: 15px; } | |
| /* Touch has no hover to reveal it with, and a button parked in the margin is the | |
| gutter this design is avoiding. There the selection bubble is the whole comment | |
| flow, and it already works. */ | |
| @media (hover: none) { | |
| #hover-comment { display: none ; } | |
| } | |
| /* Where a position-anchored comment points. No underline and no fill: the words | |
| are not what the comment is about, so marking them would be a lie. A rule out | |
| in the margin, on the same geometry as the suggestion bar (left: -16px) but in | |
| the comment colour, and only while the composer or the card is open — at rest | |
| these comments mark nothing at all. */ | |
| .tiptap .comment-anchor { position: relative; } | |
| .tiptap .comment-anchor::before { | |
| content: ''; position: absolute; left: -16px; top: 2px; bottom: 2px; | |
| width: 2.5px; border-radius: 2px; background: var(--hl-edge); | |
| } | |
| .tiptap .comment-anchor.pending::before { opacity: .55; } | |
| .tiptap .comment-anchor.active { background: var(--hl-weak); } | |
| /* --- margin cards --- */ | |
| .card { | |
| position: absolute; left: 0; width: 100%; | |
| background: var(--surface); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 10px 14px; cursor: pointer; transition: top .18s ease, box-shadow .15s, border-color .15s; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .card:hover { border-color: var(--border-strong); } | |
| .card.expanded { box-shadow: var(--shadow-md); border-color: var(--accent); cursor: default; z-index: 5; } | |
| .card.origin-related { border-color: var(--hl-edge); box-shadow: 0 0 0 2px var(--hl), var(--shadow-sm); } | |
| .card .head { | |
| display: grid; grid-template-columns: minmax(0, 1fr) auto; | |
| grid-template-areas: 'identity actions' 'context context'; | |
| align-items: center; column-gap: 8px; row-gap: 5px; min-width: 0; color: var(--text-2); | |
| } | |
| .card .head-identity { grid-area: identity; display: flex; align-items: center; gap: 7px; min-width: 0; } | |
| .card .head-actions { grid-area: actions; display: flex; align-items: center; gap: 2px; justify-self: end; min-width: max-content; } | |
| .card .head-context { grid-area: context; display: flex; align-items: center; gap: 7px; min-width: 0; } | |
| .card .head .icon { color: var(--text-3); } | |
| .card.expanded .head .icon { color: var(--accent); } | |
| .card .head .who { font-weight: 600; color: var(--text); font-size: 13px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| .card .head .who.agent::after { content: "agent"; font-size: 10px; font-weight: 600; color: var(--accent); background: var(--accent-soft); border-radius: 4px; padding: 1px 5px; margin-left: 6px; vertical-align: 1px; } | |
| .card .head .preview { font-size: 12.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; } | |
| /* the head preview is a teaser for the collapsed card — once open, the body | |
| shows the same rationale/message in full, so repeating it is just noise */ | |
| .card.expanded .head .preview { display: none; } | |
| .card .head .when { font-size: 11.5px; color: var(--text-3); flex-shrink: 0; margin-left: auto; } | |
| .card .origin-link { | |
| flex: 0 0 auto; border: 0; border-radius: 999px; padding: 2px 7px; cursor: pointer; | |
| background: var(--hl-weak); color: var(--text-3); font: 10.5px/1.35 var(--ui); | |
| } | |
| .card .origin-link:hover { background: var(--hl); color: var(--accent); } | |
| .card .body { margin-top: 10px; } | |
| .card blockquote { | |
| margin: 0 0 10px; padding: 3px 10px; border-left: 2px solid var(--hl-edge); background: var(--hl); | |
| font-size: 12.5px; color: var(--text-2); border-radius: 0 6px 6px 0; | |
| overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; | |
| } | |
| .card blockquote.orphaned { border-left-color: var(--border-strong); background: var(--tint); text-decoration: line-through; } | |
| /* A position-anchored comment's locator, in place of the blockquote. Kept | |
| visibly NOT a quote — no fill, no quote bar, smaller and greyer, with a quote | |
| glyph that reads as "near here" rather than "these words". Same click target | |
| though: it jumps to the place. */ | |
| .card .anchor-loc, #composer-loc { | |
| display: flex; align-items: center; gap: 6px; min-width: 0; | |
| margin: 0 0 10px; font-size: 12px; color: var(--text-3); cursor: pointer; | |
| } | |
| #composer-loc { margin: 0; cursor: default; } | |
| .card .anchor-loc .icon, #composer-loc .icon { width: 13px; height: 13px; flex-shrink: 0; color: var(--hl-edge); } | |
| .card .anchor-loc .words, #composer-loc .words { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; } | |
| .card .anchor-loc.orphaned .words { text-decoration: line-through; } | |
| .card .anchor-loc.orphaned .icon { color: var(--border-strong); } | |
| .msg { margin: 10px 0; font-size: 13.5px; } | |
| .msg .author { font-weight: 600; font-size: 12.5px; display: flex; align-items: center; gap: 6px; color: var(--text); } | |
| .msg .author .when { font-weight: 400; color: var(--text-3); font-size: 11.5px; } | |
| .msg .author .agent-tag { font-size: 10px; font-weight: 600; color: var(--accent); background: var(--accent-soft); border-radius: 4px; padding: 1px 5px; } | |
| .msg .text { white-space: pre-wrap; word-wrap: break-word; color: var(--text-2); margin-top: 2px; } | |
| .msg .text .mention { color: var(--accent); font-weight: 600; } | |
| .chip { | |
| display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 500; border-radius: 999px; | |
| padding: 2px 9px; margin: 4px 4px 0 0; background: var(--chip-bg); color: var(--text-2); | |
| } | |
| .chip::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--text-3); } | |
| .chip.claimed::before { background: var(--text); } | |
| .chip.done { background: var(--accent-soft); color: var(--accent); } | |
| .chip.done::before { background: var(--accent); } | |
| .chip.failed { background: var(--chip-bg); color: var(--text); text-decoration: line-through; } | |
| .reply-row { display: flex; gap: 6px; margin-top: 10px; } | |
| .reply-row textarea { | |
| font-size: 13px; resize: none; overflow-y: auto; min-height: 32px; | |
| line-height: 1.45; /* one line at rest, grows to ~4 with Shift+Enter */ | |
| } | |
| .card .actions { display: flex; gap: 8px; margin-top: 10px; justify-content: flex-end; align-items: center; } | |
| /* card header actions (accept / reject — visible even collapsed) */ | |
| .card .head .iconbtn { padding: 3px; flex-shrink: 0; } | |
| .card .head .accept-btn { color: var(--accent); } | |
| .card .head .accept-btn:hover { background: var(--accent-soft); } | |
| .card .head .reject-btn:hover { color: var(--text); } | |
| /* inline track-changes in the document */ | |
| .tiptap .sugg-del { text-decoration: line-through; text-decoration-thickness: 1.5px; color: var(--text-3); background: var(--del-bg); border-radius: 2px; cursor: pointer; } | |
| .tiptap .sugg-ins { | |
| color: var(--accent-dark); background: var(--accent-soft); box-shadow: 0 1.5px 0 var(--accent); | |
| border-radius: 2px; padding: 0 1px; cursor: pointer; white-space: pre-wrap; | |
| } | |
| .tiptap .sugg-del-block { text-decoration: line-through; color: var(--text-3); opacity: 0.75; cursor: pointer; } | |
| .tiptap .sugg-del-block.sugg-del-empty { height: 4px; overflow: hidden; margin-top: 0; } | |
| .tiptap .sugg-ins-block { | |
| border-left: 2.5px solid var(--accent); background: var(--accent-soft); | |
| padding: 8px 14px; margin: 0.5em 0; cursor: pointer; | |
| } | |
| .tiptap .sugg-ins-block > * + * { margin-top: 0.4em; } | |
| .tiptap .sugg-ins-block img { max-height: 200px; } | |
| /* the active (clicked) suggestion stands out from its neighbors */ | |
| .tiptap .sugg-ins.active { background: var(--ins-active); box-shadow: 0 2px 0 var(--accent); font-weight: 600; } | |
| .tiptap .sugg-del.active { background: var(--del-bg-active); color: var(--text-2); } | |
| .tiptap .sugg-ins-block.active { background: var(--ins-block-active); border-left-width: 4px; box-shadow: var(--shadow-sm); } | |
| .tiptap .sugg-del-block.active { opacity: 0.55; background: var(--del-bg); } | |
| .tiptap .suggestion-hl { cursor: pointer; } | |
| .tiptap .suggestion-hl.active::before { width: 3.5px; background: var(--accent); } | |
| .tiptap .suggestion-hl.active { background: var(--hl-weak); } | |
| /* zoom control, and next to it the reader text size. Two readouts side by side | |
| have to be told apart at a glance, so they are never in the same unit: zoom | |
| reads as a percentage of the page ("110%") and text size as the document's | |
| own type size ("16px") — which is also what the small-A and large-A buttons | |
| either side of it are already saying, against zoom's two magnifiers. */ | |
| #zoom-label, #docsize-label { font-size: 11.5px; color: var(--text-3); min-width: 34px; text-align: center; user-select: none; } | |
| #docsize-label { min-width: 38px; } /* "26px" is a character wider than "150%" */ | |
| /* The two A's are set in the document's own face, so they turn serif with the | |
| Reading style — the same tell the italic I in the toolbar already gives. | |
| Their sizes are the control's whole legend: a small A on the left, a large | |
| one on the right. 11.5 and 18 are as far apart as they go for free — at 18px | |
| the large A's box is 28px, still inside the 29px row that H1 and B set, so | |
| the toolbar and the header keep the heights they had. The phone row is 38px | |
| and taller still, so these two sizes carry there unchanged. */ | |
| #header-tools #text-smaller, #header-tools #text-larger { font-family: var(--doc); line-height: 1; } | |
| #header-tools #text-smaller { font-size: 11.5px; } | |
| #header-tools #text-larger { font-size: 18px; } | |
| .rationale { font-size: 13px; color: var(--text-2); margin-top: 8px; font-style: italic; } | |
| .status-note { font-size: 11.5px; color: var(--text-3); } | |
| /* composers (comment + suggest) — positioned next to the anchored text, like cards */ | |
| .composer-box { position: absolute; left: 0; width: 100%; z-index: 6; background: var(--surface); border: 1px solid var(--accent); border-radius: 10px; padding: 12px 14px; box-shadow: var(--shadow-md); display: flex; flex-direction: column; gap: 8px; } | |
| .composer-box textarea, .composer-box input { font-size: 13px; } | |
| .composer-box .row { justify-content: flex-end; } | |
| .composer-box blockquote { margin: 0; padding: 3px 10px; border-left: 2px solid var(--hl-edge); background: var(--hl); font-size: 12.5px; color: var(--text-2); border-radius: 0 6px 6px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| /* mention autocomplete */ | |
| .mention-menu { border: 1px solid var(--border); border-radius: 8px; background: var(--surface); box-shadow: var(--shadow-md); margin-top: 4px; overflow: hidden; position: relative; z-index: 8; } | |
| .mention-menu div { padding: 6px 12px; cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 7px; } | |
| .mention-menu div:hover, .mention-menu div.selected { background: var(--accent-soft); } | |
| .mention-menu div.selected { box-shadow: inset 2px 0 0 var(--accent); } | |
| .mention-menu .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border-strong); } | |
| .mention-menu .dot.on { background: var(--accent); } | |
| /* editing / suggesting switch — the header's mode control, like Docs' pencil */ | |
| /* the theme toggle is icon-only in the header; the label is for the ⋯ sheet */ | |
| #theme-btn .lbl, #docstyle-btn .lbl { display: none; } | |
| #mode-switch { display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px; background: var(--tint); flex-shrink: 0; } | |
| #mode-switch button { | |
| border: none; background: none; cursor: pointer; border-radius: 999px; | |
| padding: 4px 9px; font: inherit; font-size: 12.5px; color: var(--text-3); white-space: nowrap; | |
| } | |
| #mode-switch button:hover { color: var(--text); } | |
| #mode-switch button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); font-weight: 550; } | |
| /* suggesting mode: the document is read-only, so the text cursor would lie */ | |
| body.suggesting .tiptap { caret-color: transparent; cursor: default; } | |
| /* --- math (KaTeX) ---------------------------------------------------------- | |
| Formulas are atoms: one click opens the LaTeX source in an input, because the | |
| source is what round-trips to markdown and what agents propose. */ | |
| .math-inline { display: inline; border-radius: 3px; } | |
| .math-block { | |
| display: block; margin: 0.75em 0; text-align: center; | |
| padding: 4px 0; overflow-x: auto; overflow-y: hidden; | |
| } | |
| /* only the formula itself is clickable — a display block is as wide as the | |
| text column, and clicking empty space beside it must still move the caret */ | |
| /* manipulation: suppresses double-tap-to-zoom, so a double TAP reliably opens | |
| the source editor on a phone instead of zooming the page */ | |
| .math-view { cursor: pointer; border-radius: 3px; padding: 0 2px; touch-action: manipulation; } | |
| .math-block .math-view { display: inline-block; } | |
| .math-view:hover { background: var(--hl-weak); } | |
| .math-inline.selected .math-view, .math-block.selected .math-view { background: var(--hl); outline: 1px solid var(--hl-edge); } | |
| /* before KaTeX lands (and if it never does) the source stays readable */ | |
| .math-view.math-raw { font-family: var(--mono); font-size: 0.92em; color: var(--text-2); } | |
| .math-view.math-empty { font-family: var(--ui); font-size: 0.85em; color: var(--text-3); } | |
| /* the source editor: shown in place of the rendering, never both */ | |
| .math-src { display: none; } | |
| .math-inline.editing .math-view, .math-block.editing .math-view { display: none; } | |
| .math-inline.editing .math-src, .math-block.editing .math-src { | |
| display: inline-block; font-family: var(--mono); font-size: 0.92em; | |
| border: 1px solid var(--accent); border-radius: 4px; padding: 1px 5px; | |
| background: var(--input-bg); color: var(--text); outline: none; min-width: 60px; | |
| } | |
| .math-block.editing .math-src { display: block; margin: 0 auto; width: min(100%, 46em); text-align: center; } | |
| .math-tool-label { font-family: var(--doc); font-size: 15px; line-height: 1; } | |
| /* KaTeX renders its own colours; only the surroundings are ours */ | |
| .katex { font-size: 1.06em; } | |
| .katex-display { margin: 0; } | |
| /* --- loading skeletons ----------------------------------------------------- | |
| Shown from the first paint (they are in the HTML, not built by JS) so a slow | |
| load looks like it is working rather than finished-and-empty. */ | |
| @keyframes sk-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } } | |
| .sk-line, .sk-card { | |
| background: var(--tint); | |
| border-radius: 6px; | |
| animation: sk-pulse 1.4s ease-in-out infinite; | |
| } | |
| /* An opaque stand-in sheet with the real sheet's geometry, laid over the live | |
| editor rather than replacing it — the editor stays mounted and measurable, so | |
| card positions and zoom are already right when the skeleton lifts. */ | |
| #doc-skeleton { | |
| position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none; | |
| width: var(--sheet-w); margin: 0 auto; | |
| padding: var(--sheet-pad); min-height: var(--sheet-min-h); | |
| background: var(--page); box-shadow: var(--page-shadow); | |
| } | |
| #doc-skeleton.hidden { display: none ; } | |
| #doc-skeleton .sk-line { height: 0.85em; margin-bottom: 0.9em; } | |
| #doc-skeleton .sk-title { height: 1.9em; width: 55%; margin-bottom: 1.6em; } | |
| #doc-skeleton .sk-gap { margin-top: 2em; } | |
| .sk-line.w40 { width: 40%; } .sk-line.w70 { width: 70%; } .sk-line.w75 { width: 75%; } | |
| .sk-line.w80 { width: 80%; } .sk-line.w82 { width: 82%; } .sk-line.w88 { width: 88%; } .sk-line.w90 { width: 90%; } | |
| #docs-list.loading { display: flex; flex-direction: column; gap: 10px; } | |
| #docs-list.loading .sk-card { height: 62px; border-radius: 12px; } | |
| /* animation off for anyone who asked for less motion — the block still shows */ | |
| @media (prefers-reduced-motion: reduce) { .sk-line, .sk-card { animation: none; } } | |
| /* --- dark theme ----------------------------------------------------------- | |
| A head script stamps data-theme on <html> (stored choice, else the OS | |
| setting) before first paint, so there is no light flash and no duplicated | |
| media-query block. Everything below is a variable override — no layout, | |
| no per-component dark rules. */ | |
| :root[data-theme="dark"] { | |
| color-scheme: dark; | |
| --bg: #121213; | |
| --canvas: #121213; | |
| --surface: #1f1f22; | |
| --page: #1b1b1c; | |
| --border: #35353a; | |
| --border-strong: #4b4b52; | |
| --text: #e4e4e7; | |
| --text-2: #b3b3bb; | |
| --text-3: #85858e; | |
| --accent: #74c2a0; | |
| --accent-dark: #94d7b8; | |
| --accent-soft: rgba(116, 194, 160, 0.16); | |
| --hl: rgba(116, 194, 160, 0.17); | |
| --hl-active: rgba(116, 194, 160, 0.32); | |
| --hl-weak: rgba(116, 194, 160, 0.09); | |
| --hl-edge: rgba(116, 194, 160, 0.6); | |
| /* a wash rather than the light theme's solid amber: on the dark page a filled | |
| block would out-shout the text it is supposed to be pointing at */ | |
| --find: rgba(255, 205, 90, 0.34); | |
| --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); | |
| --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.55), 0 1px 3px rgba(0, 0, 0, 0.4); | |
| --page-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.55); | |
| --input-bg: #26262a; | |
| --tint: rgba(255, 255, 255, 0.07); | |
| --tint-strong: rgba(255, 255, 255, 0.16); | |
| --scrim: rgba(0, 0, 0, 0.62); | |
| --danger: #f28b82; | |
| --chip-bg: #30303a; | |
| --menu-solid: #2f3136; | |
| --tooltip-bg: #34363c; | |
| --on-primary: #17171a; | |
| --primary-hover: #ffffff; | |
| --ins-active: rgba(116, 194, 160, 0.32); | |
| --ins-block-active: rgba(116, 194, 160, 0.22); | |
| --del-bg: rgba(255, 255, 255, 0.08); | |
| --del-bg-active: rgba(255, 255, 255, 0.15); | |
| --sugg-bar: #5f9a83; | |
| /* the document's own ink, on Docs' dark page */ | |
| --doc-text: #e8e8e6; | |
| --doc-text-2: #c3c3c1; | |
| --doc-text-3: #a7a7a5; | |
| --doc-rule: #55555a; | |
| --table-rule: #6a6a70; | |
| --doc-link: #8ab4f8; | |
| --code-bg: #2b2b30; | |
| --code-block-bg: #26262b; | |
| --code-block-border: #3a3a40; | |
| } | |
| /* --- document style: Docs vs Reading --------------------------------------- | |
| A second axis, independent of light/dark: what the *document* is set in. | |
| The default ("docs") is Google Docs' Arial 11pt/1.15, set in :root above. | |
| "reading" is the long-form magazine setting measured off a Foreign Affairs | |
| article: a serif at 22px/1.45, space between paragraphs instead of Docs' | |
| none, and small-caps section heads. | |
| The sheet is untouched — and it happens to need no touching: Docs' 8.5in page | |
| with 1in margins leaves a 624px column, within a few pixels of the 630px the | |
| article is set to, so the measure is already right at this size. | |
| The page geometry is the one thing Reading leaves alone. No literal colour | |
| appears below either — every rule inherits --doc-text and friends, so Reading | |
| works in dark mode without a second block. A head script stamps | |
| data-doc-style before first paint, like the theme. */ | |
| :root[data-doc-style="reading"] { | |
| --doc: "Libre Baskerville", Charter, "Iowan Old Style", Georgia, "Times New Roman", serif; | |
| /* Chosen at the page rather than derived, after sliding the numbers around | |
| against real text. Measured over 37 paragraphs in this 624px column it sets | |
| 10.69 words a line on 28.8px of leading, which puts it between Docs (13.54 | |
| words on 16.9px) and the magazine (9.88 on 31.9px) and nearer the magazine. | |
| Switching styles therefore does reflow the document — Baskerville is a wide | |
| face, and no size of it both matches Docs' line and stays this open. */ | |
| --doc-size: 16px; | |
| --doc-line: 1.8; /* = 28.8px */ | |
| --para-gap: 1.1em; /* = 17.6px: paragraphs are separated by space… */ | |
| --doc-link: var(--doc-text); /* the article underlines links in the text ink */ | |
| } | |
| /* …not by indentation, so the "no space between paragraphs" Docs rule lifts */ | |
| [data-doc-style="reading"] .tiptap p { margin: 0 0 var(--para-gap); } | |
| [data-doc-style="reading"] .tiptap p:last-child { margin-bottom: 0; } | |
| [data-doc-style="reading"] .tiptap li > p { margin: 0; } | |
| /* Section heads are small caps at text weight — the magazine's own hierarchy, | |
| which leans on letterspacing and space above rather than on size and bold. | |
| font-variant is synthesised from the roman here; Libre Baskerville ships no | |
| true small-cap glyphs (the article's own face does). */ | |
| [data-doc-style="reading"] .tiptap h2, | |
| [data-doc-style="reading"] .tiptap h3 { | |
| /* 25.6px small caps, with the article's 0.075em tracking and its 2em of air | |
| above — the proportions that make a section head read as one */ | |
| font-size: 1.6em; font-variant: all-small-caps; letter-spacing: 0.075em; | |
| line-height: 1.45; margin: 2em 0 0.333em; color: var(--doc-text); | |
| } | |
| /* holds the article's 64-to-30 relationship between headline and section head */ | |
| [data-doc-style="reading"] .tiptap h1 { font-size: 3.4133em; line-height: 1.1; margin: 1.25em 0 0.208em; } | |
| [data-doc-style="reading"] .tiptap h4 { font-size: 1em; font-weight: 700; line-height: 1.3; margin: 2em 0 0.455em; color: var(--doc-text); } | |
| [data-doc-style="reading"] .tiptap h5 { font-size: 1em; font-style: italic; line-height: 1.3; margin: 2em 0 0.455em; color: var(--doc-text); } | |
| [data-doc-style="reading"] .tiptap h6 { | |
| font-size: 0.818em; font-variant: all-small-caps; letter-spacing: 0.075em; | |
| font-style: normal; line-height: 1.3; margin: 2.44em 0 0.556em; color: var(--doc-text-2); | |
| } | |
| /* nothing above the first block, whatever it is */ | |
| [data-doc-style="reading"] .tiptap > :first-child { margin-top: 0; } | |
| /* the opening heading is the article title: the display cut, set large */ | |
| [data-doc-style="reading"] .tiptap > h1:first-child { font-size: 2.909em; line-height: 1.1; margin-bottom: 0.234em; } | |
| /* an underline in the text's own ink, rather than Docs' blue */ | |
| [data-doc-style="reading"] .tiptap a { text-decoration: underline; text-decoration-thickness: 0.05em; text-underline-offset: 0.15em; } | |
| [data-doc-style="reading"] .tiptap hr { border-top-color: var(--doc-text); margin: 1.818em 0; } | |
| [data-doc-style="reading"] .tiptap blockquote { padding-left: 1.2em; font-style: italic; } | |
| /* lists sit close to the text, not at Docs' half-inch tab stop */ | |
| [data-doc-style="reading"] .tiptap ul, | |
| [data-doc-style="reading"] .tiptap ol { padding-left: 1.4em; margin: 0 0 var(--para-gap); } | |
| [data-doc-style="reading"] .tiptap li { padding: 0.227em 0; } | |
| /* --- home --- */ | |
| .home { max-width: 680px; margin: 0 auto; padding: 48px 24px 120px; } | |
| .home-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } | |
| .home h1 { font-size: 22px; letter-spacing: -0.02em; margin: 0; } | |
| .home h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-3); margin: 44px 0 6px; } | |
| .doc-card { | |
| display: flex; align-items: center; gap: 12px; background: var(--surface); border: 1px solid var(--border); | |
| border-radius: 10px; padding: 14px 18px; margin-bottom: 8px; text-decoration: none; color: var(--text); | |
| transition: border-color .12s, box-shadow .12s; | |
| } | |
| .doc-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); } | |
| .doc-card .title { font-weight: 550; } | |
| .doc-card .meta { margin-left: auto; font-size: 12.5px; color: var(--text-3); } | |
| .doc-card .iconbtn { opacity: 0; transition: opacity .12s; } | |
| .doc-card:hover .iconbtn { opacity: 1; } | |
| .home #agent-form { display: flex; gap: 8px; max-width: 440px; margin-top: 12px; } | |
| .home .agent-row { border-bottom: 1px solid var(--border); padding: 9px 2px; } | |
| .home .agent-row:last-of-type { border-bottom: none; } | |
| /* --- pages sidebar --- */ | |
| #layout { position: relative; } | |
| #sidebar { | |
| width: var(--sidebar-w); flex: 0 0 var(--sidebar-w); padding: 16px 14px 40px; box-sizing: border-box; | |
| border-right: 0; background: var(--bg); | |
| overflow-y: auto; font-size: 13.5px; | |
| } | |
| /* On a real three-column layout, Pages owns the entire gutter between the | |
| viewport edge and the sheet. The page column itself stays page-sized so all | |
| spare room goes to the navigation, rather than becoming an empty strip. */ | |
| @media (min-width: 981px) { | |
| #sidebar:not(.hidden):not(.space-drawer) { | |
| position: sticky; top: var(--hdr-h, 52px); height: calc(100vh - var(--hdr-h, 52px)); align-self: flex-start; | |
| width: auto; min-width: var(--sidebar-w); flex: 1 1 var(--sidebar-w); | |
| } | |
| #sidebar:not(.hidden):not(.space-drawer) ~ #editor-col { flex: 0 0 var(--sheet-w); } | |
| #sidebar.space-drawer { | |
| position: absolute; left: var(--layout-edge); top: 24px; z-index: 46; width: var(--sidebar-w); min-height: 320px; | |
| background: var(--bg); border: 1px solid var(--line); box-shadow: var(--shadow-md); | |
| } | |
| #sidebar.space-drawer #sidebar-close { display: inline-flex ; } | |
| } | |
| #sidebar.hidden { display: none; } | |
| #sidebar-sticky-head { position: sticky; top: -16px; z-index: 4; padding: 16px 0 10px; background: var(--bg); } | |
| #sidebar-head { display: flex; align-items: center; gap: 5px; } | |
| .sidebar-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 2px; } | |
| #sidebar-head .sidebar-actions .iconbtn { width: 27px; height: 27px; color: var(--text-3); } | |
| #sidebar-head .sidebar-actions .iconbtn:hover { color: var(--text); background: var(--hl-weak); } | |
| #sidebar-head .sidebar-actions .icon { width: 16px; height: 16px; } | |
| #sidebar-search-wrap { position: relative; flex: 1 1 auto; min-width: 0; } | |
| #sidebar-search-wrap::before { | |
| content: '⌕'; position: absolute; left: 9px; top: 50%; transform: translateY(-50%); z-index: 1; | |
| color: var(--text-3); font-size: 15px; pointer-events: none; | |
| } | |
| #sidebar-search { | |
| width: 100%; box-sizing: border-box; padding: 8px 44px 8px 28px; border: 1px solid var(--line); | |
| border-radius: 6px; outline: none; background: var(--surface); color: var(--text); font: inherit; font-size: 13px; | |
| } | |
| #sidebar-search:focus { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 13%, transparent); } | |
| #sidebar-search-key { | |
| position: absolute; right: 7px; top: 50%; transform: translateY(-50%); border: 1px solid var(--line); | |
| border-radius: 4px; padding: 2px 6px; background: var(--bg); color: var(--text-3); | |
| font: 10px/1.2 var(--mono, ui-monospace, monospace); cursor: pointer; | |
| } | |
| #sidebar-search-key:hover { border-color: var(--accent); color: var(--accent); } | |
| #sidebar-search:not(:placeholder-shown) + #sidebar-search-key { font-size: 0; } | |
| #sidebar-search:not(:placeholder-shown) + #sidebar-search-key::after { content: '×'; font: 15px/1 sans-serif; } | |
| #page-tree ul { list-style: none; margin: 0; padding: 0; } | |
| #page-tree > ul { padding-left: 0; } | |
| #page-tree li { margin: 0; } | |
| #page-tree li > ul { margin: 1px 0 0 9px; padding-left: 16px; border-left: 1px solid var(--line); } | |
| #page-tree .nav-cat { margin-bottom: 10px; } | |
| #page-tree .nav-cat .nav-cat { margin-bottom: 2px; } | |
| #page-tree .tree-row { | |
| --sidebar-row-bg: var(--bg); | |
| position: relative; display: flex; align-items: center; min-width: 0; border-left: 2px solid transparent; border-radius: 5px; | |
| } | |
| #page-tree a { | |
| display: block; flex: 1; min-width: 0; padding: 5px 9px; color: var(--text-2); font-size: 12.5px; line-height: 1.35; text-decoration: none; | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| transition: background .12s, color .12s; | |
| } | |
| #page-tree .page-row:hover { --sidebar-row-bg: var(--hl-weak); background: var(--sidebar-row-bg); } | |
| #page-tree .page-row:hover > a { color: var(--text); } | |
| /* the tint plus the accent-coloured label is enough to say "you are here" — | |
| the left rule was a third marker for the same fact */ | |
| #page-tree .page-row:has(a.current) { | |
| --sidebar-row-bg: color-mix(in srgb, var(--accent) 12%, transparent); | |
| background: var(--sidebar-row-bg); | |
| } | |
| #page-tree a.current { color: var(--accent); font-weight: 500; } | |
| #page-tree .tree-row[data-selected='1']:not(:has(a.current)) { --sidebar-row-bg: var(--hl); background: var(--sidebar-row-bg); } | |
| #page-tree .tree-row[data-selected='1']:not(:has(a.current)) a, | |
| #page-tree .group-row[data-selected='1'] .group-label { color: var(--text); } | |
| #page-tree a.ghost-page { color: var(--text-3); font-style: italic; } | |
| #page-tree .tree-caret, | |
| #page-tree .tree-caret-spacer { | |
| width: 16px; height: 26px; flex: 0 0 16px; margin: 0; padding: 0; border: 0; background: transparent; | |
| } | |
| #page-tree .tree-caret { | |
| color: var(--text-3); cursor: pointer; font-size: 18px; line-height: 26px; transform: rotate(0deg); | |
| align-self: center; text-align: center; transition: transform .15s, color .12s; | |
| } | |
| #page-tree .tree-caret.collapsed { transform: rotate(-90deg); } | |
| #page-tree .tree-caret:hover { color: var(--text); } | |
| #page-tree .tree-row:hover > .tree-caret { color: var(--text-2); } | |
| #page-tree .group-row { align-items: baseline; gap: 7px; width: 100%; transition: background .12s; } | |
| #page-tree .group-label { | |
| flex: 1; min-width: 0; border: 0; background: transparent; color: var(--text); border-radius: 5px; | |
| padding: 5px 6px; font: inherit; font-size: 13.5px; font-weight: 600; | |
| text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; | |
| } | |
| #page-tree .group-row.lvl-2 .group-label { font-size: 12.5px; } | |
| #page-tree .group-row.lvl-3 .group-label, | |
| #page-tree .group-row.lvl-4 .group-label { color: var(--text-2); font-size: 12px; } | |
| #page-tree .group-row:hover { --sidebar-row-bg: var(--hl-weak); background: var(--sidebar-row-bg); } | |
| #page-tree .home-star { | |
| width: 12px; height: 12px; flex: 0 0 12px; color: var(--text-3); | |
| /* out of the flow: the row's caret gutter is 16px, and the star sits in it so | |
| home's title lines up with every other page's */ | |
| position: absolute; left: 2px; top: 50%; transform: translateY(-50%); pointer-events: none; | |
| } | |
| /* home with children: the caret owns the gutter, so the star trails the title | |
| instead of landing on top of it — still without moving the title */ | |
| #page-tree .home-star.trailing { position: static; transform: none; margin: 0 4px 0 2px; } | |
| #page-tree .sidebar-rename { | |
| flex: 1; min-width: 0; margin: 1px 3px; padding: 3px 5px; border: 1px solid var(--accent); border-radius: 5px; | |
| background: var(--surface); color: var(--text); font: inherit; font-size: 13px; | |
| } | |
| #page-tree .unfiled-label { color: var(--text-3); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 10px 0 2px; } | |
| /* --- what a search looks like in the tree ---------------------------------- | |
| Only ever present while the box has a query in it, which is why none of this | |
| touches the row rules above: the unfiltered tree renders exactly as before. | |
| The title line becomes a flex line when it carries a hit count, so the count | |
| keeps its width and the title is the part that ellipses. */ | |
| #page-tree a:has(.tree-hit-count) { display: flex; align-items: baseline; gap: 5px; } | |
| #page-tree .tree-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| /* the matched substring, in a title and in a snippet alike */ | |
| #page-tree a b { color: var(--accent); font-weight: 600; } | |
| /* The page's true number of hits, not the number of snippets shown below it. */ | |
| #page-tree .tree-hit-count { | |
| flex: 0 0 auto; padding: 0 5px; border-radius: 8px; background: var(--accent-soft); | |
| color: var(--accent); font-size: 9.5px; line-height: 15px; font-weight: 600; | |
| font-family: var(--mono, ui-monospace, monospace); | |
| } | |
| /* One row per occurrence, indented under its page and ruled on the left so a | |
| long list still reads as belonging to the page above it. */ | |
| #page-tree .tree-hits { margin: 1px 0 4px 25px; } | |
| #page-tree .tree-hit { | |
| display: block; padding: 2px 6px; border-left: 2px solid var(--line); border-radius: 0 4px 4px 0; | |
| color: var(--text-3); font-size: 11px; line-height: 1.4; text-decoration: none; | |
| white-space: normal; overflow: visible; text-overflow: clip; | |
| transition: background .12s, border-color .12s, color .12s; | |
| } | |
| #page-tree .tree-hit:hover { background: var(--hl-weak); border-left-color: var(--accent); color: var(--text-2); } | |
| /* The heading the hit sits under. rl-wiki put this in the URL as an anchor; | |
| cowrite opens an editor, which has no heading anchors, so the heading earns | |
| its keep as the visible answer to "where in the page is this?". | |
| What that costs: two sections with the same name are the same label here, | |
| where rl-wiki's slugs disambiguated them (dup, dup-1). Not worth reviving the | |
| slugify + de-dup pass for — the labels are only for reading, the jump is by | |
| ordinal and stays exact either way, and the snippet text is what actually | |
| tells two hits apart. */ | |
| #page-tree .tree-hit-sec { | |
| display: block; color: var(--text-3); font-size: 9.5px; text-transform: uppercase; | |
| letter-spacing: 0.05em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |
| } | |
| #page-tree .tree-hit.more { font-style: italic; color: var(--text-3); border-left-color: transparent; } | |
| #page-tree .tree-hit.more:hover { background: none; } | |
| #page-tree .tree-empty { color: var(--text-3); font-size: 11.5px; font-style: italic; padding: 8px 2px; } | |
| .sidebar-toast { | |
| position: fixed; left: 16px; bottom: 16px; z-index: 90; max-width: min(380px, calc(100vw - 32px)); | |
| padding: 8px 12px; border-radius: 999px; background: var(--tooltip-bg); color: #fff; | |
| box-shadow: var(--shadow-md); font-size: 12.5px; line-height: 1.35; opacity: 0; | |
| transform: translateY(6px); pointer-events: none; transition: opacity .15s, transform .15s; | |
| } | |
| .sidebar-toast.show { opacity: 1; transform: translateY(0); } | |
| #sidebar-toggle { | |
| position: absolute; left: 0; top: 10px; z-index: 5; width: 22px; height: 26px; | |
| border: 1px solid var(--line); border-left: none; border-radius: 0 6px 6px 0; | |
| background: var(--paper, #fff); cursor: pointer; color: var(--text-3); font-size: 11px; padding: 0; | |
| } | |
| #sidebar-toggle.hidden { display: none; } | |
| #sidebar-toggle:hover { color: var(--text); } | |
| /* --- image layout bubble --- */ | |
| #image-bubble { | |
| position: fixed; z-index: 60; display: flex; align-items: center; gap: 2px; | |
| background: var(--paper, #fff); border: 1px solid var(--line); border-radius: 8px; | |
| padding: 3px 5px; box-shadow: 0 4px 18px rgba(0,0,0,0.12); | |
| } | |
| #image-bubble.hidden { display: none; } | |
| #image-bubble .iconbtn { font-size: 12px; min-width: 26px; } | |
| #image-bubble .iconbtn.on { background: var(--hl); color: var(--accent); font-weight: 700; } | |
| #image-bubble .sep { width: 1px; height: 16px; background: var(--line); margin: 0 4px; } | |
| .tiptap img[data-align='center'] { display: block; margin-left: auto; margin-right: auto; } | |
| .tiptap img[data-align='right'] { display: block; margin-left: auto; } | |
| .tiptap img[data-align='left'] { display: block; margin-right: auto; } | |
| /* --- revision chain nav --- */ | |
| .chain-nav { display: inline-flex; align-items: center; gap: 2px; flex: 0 0 auto; } | |
| .chain-nav .iconbtn { min-width: 18px; padding: 0 3px; font-size: 13px; } | |
| .chain-nav .iconbtn:disabled { opacity: 0.3; cursor: default; } | |
| .chain-pos { font-size: 11px; color: var(--text-3); min-width: 26px; text-align: center; } | |
| /* Closing a thread is neither approval nor a destructive act, so the button in | |
| the head is a plain ghost — the accent green belongs to accepting a change. */ | |
| .card.thread .head .close-btn { flex-shrink: 0; padding: 3px 9px; font-size: 12px; } | |
| /* --- page-link autocomplete + links --- */ | |
| #pagelink-menu { position: fixed; z-index: 70; } | |
| .tiptap a { cursor: pointer; } | |
| /* stale-bundle banner */ | |
| #update-banner { | |
| position: fixed; top: 0; left: 0; right: 0; z-index: 100; padding: 10px 16px; | |
| background: #b45309; color: #fff; font-size: 13.5px; text-align: center; | |
| } | |
| #update-banner a { color: #fff; font-weight: 700; } | |
| /* --- diff rendering refinements --- */ | |
| /* multi-line insertions: the outer span is a pure container so its inline | |
| underline can't paint artifacts around block-level children */ | |
| .tiptap .sugg-ins.sugg-ins-multi { display: contents; } | |
| .sugg-ins-seg, | |
| .tiptap .sugg-ins-li, | |
| .tiptap .sugg-ins-codeline { | |
| color: var(--accent-dark); background: var(--accent-soft); box-shadow: 0 1.5px 0 var(--accent); | |
| border-radius: 2px; padding: 0 1px; cursor: pointer; | |
| } | |
| /* a real list-item, so the marker aligns exactly with native bullets */ | |
| .tiptap .sugg-ins-li { display: list-item; list-style: disc outside; margin-top: 2px; width: fit-content; } | |
| .tiptap .sugg-ins-li::marker { color: var(--accent); } | |
| .tiptap .sugg-ins-li.ordered { list-style: none; } | |
| .tiptap .sugg-ins-li.ordered::before { content: '+ '; color: var(--accent); font-weight: 700; } | |
| .tiptap .sugg-ins-codeline { display: block; width: fit-content; white-space: pre; min-height: 1.2em; } | |
| .tiptap .sugg-ins-multi.active .sugg-ins-li, | |
| .tiptap .sugg-ins-multi.active .sugg-ins-seg, | |
| .tiptap .sugg-ins-multi.active .sugg-ins-codeline { background: var(--ins-active); font-weight: 600; } | |
| /* pieces that continue the SAME suggestion (js-assigned) bridge the gap to | |
| the previous piece's bar — generous: overshoot only ever overlaps this | |
| suggestion's own bar, never an unrelated one */ | |
| .tiptap .suggestion-hl.hl-join::before, | |
| .tiptap .sugg-ins-block.hl-join::before { top: calc(-1.8em - 4px); } | |
| /* insertion panels: accent bar in the gutter, aligned with range bars */ | |
| .tiptap .sugg-ins-block { border-left: none; position: relative; } | |
| .tiptap .sugg-ins-block::before { | |
| content: ''; position: absolute; left: -16px; top: 0; bottom: 0; width: 2.5px; background: var(--sugg-bar); | |
| } | |
| .tiptap .sugg-ins-block.active::before { width: 3.5px; background: var(--accent); } | |
| /* --- hideable comments margin --- */ | |
| #margin-col.hidden { display: none; } | |
| #margin-toggle { | |
| position: absolute; right: 0; top: 10px; z-index: 5; width: 22px; height: 26px; | |
| border: 1px solid var(--line); border-right: none; border-radius: 6px 0 0 6px; | |
| background: var(--surface); cursor: pointer; color: var(--text-3); font-size: 11px; padding: 0; | |
| } | |
| #margin-toggle:hover { color: var(--text); } | |
| /* Trash overlays the row's trailing edge instead of shortening every title. | |
| Its opaque hover-matched fill lets text disappear cleanly underneath. */ | |
| #page-tree .page-del, #page-tree .group-del { | |
| position: absolute; right: 0; top: 50%; z-index: 2; width: 22px; height: 26px; margin: 0; padding: 0 2px; | |
| transform: translateY(-50%); background: var(--sidebar-row-bg); | |
| visibility: hidden; opacity: 0; pointer-events: none; transition: opacity .12s, color .12s, background .12s; | |
| } | |
| #page-tree .tree-row:hover > .page-del, #page-tree .tree-row:hover > .group-del, | |
| #page-tree .page-del.arming, #page-tree .group-del.arming { | |
| visibility: visible; opacity: 1; pointer-events: auto; | |
| } | |
| #page-tree .page-del svg, #page-tree .group-del svg { width: 13px; height: 13px; } | |
| #page-tree .page-del:hover, #page-tree .group-del:hover { color: var(--danger); } | |
| #page-tree .page-del.arming, #page-tree .group-del.arming { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, var(--sidebar-row-bg)); } | |
| /* --- slug legend on the structure page --- */ | |
| #slug-legend { | |
| margin-top: 14px; padding: 14px 20px; background: var(--surface); | |
| border: 1px solid var(--border); border-radius: 12px; font-size: 13px; | |
| } | |
| #slug-legend .legend-title { color: var(--text-3); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; } | |
| #slug-legend table { border-collapse: collapse; width: 100%; } | |
| #slug-legend th { text-align: left; font-size: 11.5px; color: var(--text-3); font-weight: 600; padding: 4px 12px 4px 0; border-bottom: 1px solid var(--line); } | |
| #slug-legend td { padding: 5px 12px 5px 0; border-bottom: 1px solid var(--line); } | |
| #slug-legend tr:last-child td { border-bottom: none; } | |
| #slug-legend td a { color: var(--text); text-decoration: none; } | |
| #slug-legend td a:hover { text-decoration: underline; } | |
| #slug-legend code { | |
| background: var(--tint); padding: 1px 7px; border-radius: 5px; cursor: pointer; | |
| font-size: 12px; | |
| } | |
| #slug-legend code:hover { background: var(--hl); } | |
| #slug-legend code.copied { background: var(--accent-soft); color: var(--accent); } | |
| /* compact structure page: no phantom card height, hint above the field */ | |
| .structure-mode #editor { min-height: 0; padding: 22px 28px; } | |
| #structure-hint { | |
| margin-bottom: 12px; padding: 10px 14px; font-size: 12.5px; color: var(--text-2); | |
| background: var(--hl-weak); border: 1px solid var(--line); border-radius: 10px; | |
| } | |
| /* --- agents panel alignment --- */ | |
| #agents-list .agent-row { | |
| display: flex; align-items: center; gap: 8px; padding: 5px 0; | |
| border-bottom: 1px solid var(--line); | |
| } | |
| #agents-list .agent-row:last-child { border-bottom: none; } | |
| #agents-list .agent-id { display: flex; flex-direction: column; min-width: 0; flex: 1; line-height: 1.25; } | |
| #agents-list .agent-id .handle { font-weight: 600; font-size: 13px; } | |
| #agents-list .agent-id .owner { font-size: 11px; } | |
| #agents-list .actions { display: flex; gap: 2px; flex: 0 0 auto; } | |
| #agents-list .actions .iconbtn { width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center; } | |
| #agents-list .actions .iconbtn.on { color: var(--accent); } | |
| #agents-list .actions .iconbtn svg { width: 15px; height: 15px; } | |
| .key-box .key-label { font-size: 12px; color: var(--text-2); } | |
| .key-box .row { display: flex; gap: 6px; flex-wrap: wrap; } | |
| .key-box .row .btn { white-space: nowrap; } | |
| /* --- instant tooltips for icon buttons --- */ | |
| [data-tip] { position: relative; } | |
| [data-tip]:hover::after { | |
| content: attr(data-tip); | |
| position: absolute; bottom: calc(100% + 7px); right: -4px; | |
| background: var(--tooltip-bg); color: #fff; font-size: 11.5px; line-height: 1.35; | |
| padding: 5px 9px; border-radius: 7px; width: max-content; max-width: 240px; | |
| white-space: normal; text-align: left; z-index: 95; pointer-events: none; | |
| box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22); | |
| } | |
| [data-tip].tip-below:hover::after { bottom: auto; top: calc(100% + 7px); } | |
| /* --- task lists --- */ | |
| .tiptap ul[data-type='taskList'] { list-style: none; padding-left: 4px; margin: 0.4em 0; } | |
| .tiptap ul[data-type='taskList'] li { display: flex; align-items: flex-start; gap: 8px; margin: 3px 0; } | |
| .tiptap ul[data-type='taskList'] li > label { margin-top: 2px; flex: 0 0 auto; } | |
| .tiptap ul[data-type='taskList'] li > div { flex: 1; } | |
| .tiptap ul[data-type='taskList'] input[type='checkbox'] { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; } | |
| .tiptap ul[data-type='taskList'] li[data-checked='true'] > div { color: var(--text-3); text-decoration: line-through; } | |
| /* task lists rendered in suggestion insertion panels */ | |
| .sugg-ins-block ul[data-type='taskList'] { list-style: none; padding-left: 0; } | |
| .sugg-ins-block .task-item { display: flex; align-items: flex-start; gap: 8px; } | |
| .sugg-ins-block .task-item.checked > span { color: var(--text-3); text-decoration: line-through; } | |
| /* --- tables --- */ | |
| .tiptap table, .sugg-ins-block table { border-collapse: collapse; width: 100%; margin: 0.4em 0; font-size: 1em; table-layout: fixed; overflow: hidden; } | |
| /* Docs draws a 1pt dark rule and pads cells ~5pt; no shaded header row */ | |
| .tiptap th, .tiptap td, .sugg-ins-block th, .sugg-ins-block td { | |
| border: 1px solid var(--table-rule); padding: 5pt 7pt; vertical-align: top; text-align: left; | |
| } | |
| .tiptap th, .sugg-ins-block th { background: none; font-weight: 700; } | |
| .tiptap td p, .tiptap th p { margin: 0; } | |
| .tiptap .selectedCell:after { | |
| content: ''; position: absolute; inset: 0; background: var(--hl); pointer-events: none; | |
| } | |
| .tiptap td, .tiptap th { position: relative; } | |
| /* Column resizing. prosemirror-tables draws the handle itself and puts | |
| .resize-cursor on the editor while one is grabbed; both need styling or the | |
| feature is invisible. The handle sits on the cell border it moves. */ | |
| .tiptap .column-resize-handle { | |
| position: absolute; right: -1px; top: 0; bottom: -1px; width: 3px; | |
| background: var(--accent, #1a73e8); pointer-events: none; z-index: 5; | |
| } | |
| .tiptap.resize-cursor, .tiptap.resize-cursor * { cursor: col-resize; } | |
| /* --- table controls bar --- */ | |
| #table-controls { | |
| position: fixed; z-index: 60; display: flex; align-items: center; gap: 2px; | |
| background: var(--paper, #fff); border: 1px solid var(--line); border-radius: 8px; | |
| padding: 3px 5px; box-shadow: 0 4px 18px rgba(0,0,0,0.12); | |
| } | |
| #table-controls.hidden { display: none; } | |
| #table-controls .iconbtn { font-size: 11.5px; padding: 3px 7px; white-space: nowrap; } | |
| #table-controls .iconbtn.danger:hover { color: var(--danger); } | |
| #table-controls .sep { width: 1px; height: 16px; background: var(--line); margin: 0 3px; } | |
| /* --- proposed new pages --- */ | |
| #page-tree .proposal-row { opacity: .92; } | |
| #page-tree a.pending-page { | |
| color: var(--text-2); border: 1px dashed var(--line); border-radius: 6px; | |
| padding: 4px 8px; /* the dashed border adds 1px on each side; keep the text on the same baseline as its siblings */ | |
| } | |
| #page-tree .proposal-row:hover { --sidebar-row-bg: var(--hl-weak); background: var(--sidebar-row-bg); } | |
| #page-tree a.pending-page.current { color: var(--accent); border-color: var(--accent); } | |
| #page-tree a.pending-page .pending-badge { | |
| display: inline-block; margin-left: 6px; font-size: 10px; padding: 0 5px; border-radius: 5px; | |
| background: var(--hl); color: var(--accent); vertical-align: middle; | |
| } | |
| #proposal-view.hidden { display: none; } | |
| #proposal-view { | |
| background: var(--surface); border: none; border-radius: 0; | |
| width: var(--sheet-w); margin: 0 auto; | |
| min-height: 78vh; box-shadow: var(--page-shadow); overflow: hidden; | |
| } | |
| .proposal-bar { | |
| display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; | |
| padding: 14px 20px; border-bottom: 1px solid var(--line); background: var(--hl-weak); | |
| } | |
| .proposal-meta { font-size: 13px; color: var(--text-2); } | |
| .proposal-actions { display: flex; gap: 8px; } | |
| .proposal-content { padding: var(--sheet-pad); } | |
| .proposal-content::before { | |
| content: 'Preview of the proposed page — accept to create it'; display: block; | |
| font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-3); margin-bottom: 16px; | |
| } | |
| /* --- HTML embeds (sandboxed iframe) --- */ | |
| .html-embed-wrap { position: relative; margin: 0.6em 0; } | |
| .tiptap .html-embed, .proposal-content .html-embed { | |
| /* keeps a light canvas in either theme — the embed's HTML is not ours to re-colour */ | |
| width: 100%; border: 1px solid var(--border); border-radius: 8px; background: #fff; | |
| display: block; min-height: 24px; | |
| /* content-box so the JS-set height is the inner viewport exactly — with | |
| border-box the 1px borders shrink the viewport below the content and | |
| produce a 2px vertical scrollbar */ | |
| box-sizing: content-box; | |
| } | |
| .html-embed-wrap.ProseMirror-selectednode { outline: 3px solid var(--accent-soft); outline-offset: 2px; border-radius: 8px; } | |
| .html-embed-edit { | |
| position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity .12s; | |
| font-size: 12px; padding: 3px 9px; border-radius: 6px; cursor: pointer; | |
| border: 1px solid var(--border-strong); background: var(--surface); color: var(--text-2); | |
| } | |
| .html-embed-wrap:hover .html-embed-edit { opacity: 1; } | |
| .html-embed-edit:hover { color: var(--text); border-color: var(--text-3); } | |
| /* embed authoring modal */ | |
| .embed-overlay { | |
| position: fixed; inset: 0; z-index: 120; background: rgba(18,18,24,.4); backdrop-filter: blur(2px); | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| .embed-overlay-box { | |
| background: var(--surface); border: 1px solid var(--border); border-radius: 12px; | |
| width: min(760px, 92vw); padding: 20px; box-shadow: 0 16px 48px rgba(0,0,0,.22); | |
| } | |
| .embed-overlay-title { font-size: 15px; font-weight: 600; margin-bottom: 3px; } | |
| .embed-overlay-hint { font-size: 12.5px; color: var(--text-3); margin-bottom: 12px; } | |
| .embed-overlay-ta { | |
| width: 100%; height: 300px; resize: vertical; font: 13px/1.55 var(--mono, ui-monospace, monospace); | |
| border: 1px solid var(--border-strong); border-radius: 8px; padding: 12px; background: var(--input-bg); color: var(--text); | |
| } | |
| .embed-overlay-bar { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; } | |
| /* ============================================================ | |
| Narrow windows (≤980px: phones, tablets, split-screen). There is no | |
| room for three columns, so the page tree becomes a slide-over drawer | |
| and the comments become a bottom sheet over the document. Stacking | |
| them *under* the document instead would hide every card behind a | |
| full page scroll. | |
| ============================================================ */ | |
| @media (max-width: 980px) { | |
| #layout { gap: 0; } | |
| #margin-head { display: flex; } | |
| /* 20px of canvas each side is 40px the page wants: an 844px landscape handset | |
| holds an 816px sheet and nothing else, and paid 12px of sideways scroll for | |
| the gutter, so the gutter is the thing that yields. | |
| That handset is the case that motivated it, not the extent of it: this block | |
| is ≤980, so the narrower gutter lands on every window from 841 to 980 — | |
| desktop ones included — and that is the point rather than a side effect. The | |
| band 841-980 is where the sheet is still a fixed-width page but the canvas | |
| around it has run out, and it is the last place a reader pays for margin | |
| they cannot see. Measured at 841: main scrolls #editor-col by 15px, this | |
| branch by 0. Below 840 the sheet is fluid and the gutter costs nothing. */ | |
| :root { --layout-edge: 8px; } | |
| /* the formatting toolbar gets its own full-width row: sharing one row with | |
| the title and the actions squeezed the title down to "Pl…" */ | |
| #topbar { flex-wrap: wrap; align-content: center; height: auto; min-height: 52px; } | |
| #doc-title { max-width: 42vw; } | |
| #header-tools { | |
| order: 10; flex-basis: 100%; width: 100%; margin: 0; | |
| padding: 3px 0 1px; border-top: 1px solid var(--border); | |
| } | |
| /* --- pages drawer --- */ | |
| #sidebar { | |
| position: fixed; left: 0; top: var(--hdr-h, 50px); bottom: 0; width: 270px; | |
| z-index: 49; padding: 16px 12px 24px; | |
| background: var(--bg); box-shadow: 6px 0 28px rgba(0, 0, 0, 0.18); | |
| } | |
| #sidebar-head { gap: 6px; } | |
| #sidebar-sticky-head { background: var(--bg); } | |
| #sidebar-close { display: inline-flex ; } | |
| /* the tabs live inside #layout, which scrolls: pin them to the viewport so | |
| the drawer and the sheet stay one tap away at any scroll position */ | |
| #sidebar-toggle, #margin-toggle { position: fixed; top: calc(var(--hdr-h, 52px) + 10px); z-index: 47; } | |
| #margin-toggle { right: 0; } | |
| #scrim { position: fixed; inset: 0; z-index: 48; background: var(--scrim); } | |
| #scrim.hidden { display: none; } | |
| /* --- comments: bottom sheet --- */ | |
| /* the open sheet hides the bottom ~3/4 of the viewport: without extra trailing | |
| space the end of the document could never be scrolled into the visible strip */ | |
| body.sheet-open #layout { padding-bottom: 82vh; } | |
| #margin-col { | |
| position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: auto; | |
| margin-left: 0; pointer-events: auto; | |
| max-height: 74vh; z-index: 55; display: flex; flex-direction: column; padding: 0; | |
| background: var(--surface); border-top: 1px solid var(--border-strong); | |
| border-radius: 16px 16px 0 0; box-shadow: 0 -10px 34px rgba(0, 0, 0, 0.2); | |
| transition: transform .22s ease; | |
| } | |
| /* kept in the layout (so counts/measurements stay live) but slid away */ | |
| #margin-col.hidden { display: flex ; transform: translateY(105%); pointer-events: none; } | |
| #margin-head { | |
| align-items: center; gap: 8px; padding: 10px 8px 6px 16px; position: relative; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| #margin-grab { | |
| position: absolute; top: 5px; left: 50%; transform: translateX(-50%); | |
| width: 38px; height: 4px; border-radius: 2px; background: var(--border-strong); | |
| } | |
| #margin-head-title { font-weight: 650; font-size: 14px; flex: 1; } | |
| #margin-close { min-width: 38px; height: 38px; } | |
| #margin-tools { margin: 0; padding: 8px 16px 0; font-size: 13px; } | |
| #margin-items { | |
| flex: 1; min-height: 0 ; overflow-y: auto; -webkit-overflow-scrolling: touch; | |
| overscroll-behavior: contain; padding: 12px 14px calc(20px + env(safe-area-inset-bottom, 0px)); | |
| } | |
| .margin-empty { padding: 4px 4px 12px; } | |
| .card, .composer-box { position: static ; top: auto ; width: 100%; margin-bottom: 10px; } | |
| .card { padding: 12px 14px; } | |
| /* the preview drops to its own full-width line instead of being squeezed to | |
| "Adds …" between the author and the accept/reject buttons */ | |
| .card .head { gap: 8px; flex-wrap: wrap; row-gap: 4px; } | |
| .card .head .preview { | |
| order: 10; flex: 1 0 100%; font-size: 13px; white-space: normal; text-overflow: clip; | |
| display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; | |
| } | |
| .card .head .when { margin-left: auto; } | |
| .card .head .iconbtn { min-width: 36px; height: 36px; padding: 6px; } | |
| .msg { font-size: 14.5px; } | |
| .msg .text.clamped, .rationale.clamped { max-height: 12em; } | |
| .chip { font-size: 12px; padding: 4px 10px; } | |
| .reply-row { gap: 8px; } | |
| .card .actions .btn, .composer-box .btn { padding: 9px 14px; font-size: 14px; } | |
| } | |
| /* ============================================================ | |
| Phones (≤700px), on top of the ≤980 rules above: a compact single-row | |
| header with the crowded-out controls in a ⋯ sheet, the formatting bar as | |
| a toggleable second row, a full-bleed document and touch-sized targets. | |
| ============================================================ */ | |
| @media (max-width: 700px) { | |
| .phone-only { display: inline-flex ; } | |
| /* --- header: one compact row --- */ | |
| #topbar { | |
| height: 50px; min-height: 50px; gap: 2px; padding: 0 6px; | |
| /* wrap is what lets the formatting strip become a second row */ | |
| flex-wrap: wrap; align-content: center; z-index: 50; | |
| } | |
| #back-link { padding: 8px 4px; } | |
| #doc-title { flex: 1; min-width: 0; max-width: none; font-size: 15px; margin-left: 2px; } | |
| #conn { margin-right: 2px; } | |
| /* moved into the ⋯ sheet by js, or simply not worth the width. Scoped to the | |
| document shell: the home page header has no ⋯ menu to move them into. */ | |
| .page-doc #presence, .page-doc #whoami, | |
| .page-doc #share-btn, .page-doc #agents-btn, .page-doc #reset-btn { display: none; } | |
| #topbar .iconbtn { min-width: 40px; height: 40px; border-radius: 9px; } | |
| #m-fmt { font-weight: 700; font-size: 14px; letter-spacing: -0.02em; } | |
| #m-more { font-size: 20px; line-height: 1; padding-bottom: 6px; } | |
| #topbar .iconbtn.on { background: var(--accent-soft); color: var(--accent); } | |
| /* unread/total comment count on the comments button */ | |
| #m-comments { position: relative; } | |
| #m-comments .count { | |
| position: absolute; top: 2px; right: 1px; min-width: 15px; height: 15px; padding: 0 3px; | |
| border-radius: 8px; background: var(--accent); color: var(--on-primary); | |
| font-size: 9.5px; font-weight: 700; line-height: 15px; text-align: center; | |
| } | |
| /* formatting toolbar: a second header row, revealed by the Aa button. | |
| It scrolls horizontally instead of squeezing the header. */ | |
| #header-tools { display: none; } | |
| body.fmt-open #topbar { height: auto; padding-bottom: 5px; } | |
| body.fmt-open #header-tools { | |
| display: flex; order: 10; flex-basis: 100%; width: 100%; margin: 0; | |
| gap: 2px; padding-top: 2px; border-top: 1px solid var(--border); | |
| overflow-x: auto; overscroll-behavior-x: contain; | |
| } | |
| #header-tools button { min-width: 40px; height: 38px; font-size: 13.5px; } | |
| #header-tools .sep { height: 22px; } | |
| #zoom-label, #docsize-label { min-width: 40px; font-size: 12.5px; } | |
| /* --- popovers become near-fullwidth sheets under the header --- */ | |
| #share-pop, #agents-pop, #more-pop { | |
| position: fixed; left: 8px; right: 8px; top: calc(var(--hdr-h, 50px) + 6px); | |
| width: auto; max-height: calc(100vh - var(--hdr-h, 50px) - 20px); | |
| overflow-y: auto; z-index: 60; -webkit-overflow-scrolling: touch; | |
| } | |
| #link-pop { left: 8px ; right: 8px; width: auto; top: calc(var(--hdr-h, 50px) + 6px) ; } | |
| #more-pop { | |
| background: var(--surface); border: 1px solid var(--border); border-radius: 12px; | |
| box-shadow: var(--shadow-md); padding: 6px; | |
| } | |
| #more-pop.hidden { display: none ; } | |
| #more-pop .btn, #more-pop #whoami, #more-pop .row { | |
| display: flex ; width: 100%; justify-content: flex-start; | |
| padding: 11px 12px; border: none; border-radius: 9px; font-size: 15px; background: transparent; | |
| } | |
| #more-pop .btn + .btn { margin-top: 1px; } | |
| #more-pop .btn:hover { background: var(--tint); } | |
| #more-pop #whoami { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 12px; gap: 8px; } | |
| #more-pop .pop-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-3); padding: 8px 12px 4px; } | |
| /* --- layout: the sheet goes edge to edge --- | |
| The document's own geometry is not set here but in the ≤840px block below, | |
| which is where the page becomes a fluid column; read that comment for why. | |
| All this does is give it the whole width to be fluid in — no canvas gutter | |
| is worth 40 of the 390px a handset has. */ | |
| #layout { padding: 0 0 32vh; gap: 0; } | |
| .proposal-bar { padding: 12px 14px; } | |
| /* phone-sized drawer + sheet touch targets */ | |
| #sidebar { width: min(84vw, 300px); padding: 14px 12px 24px; font-size: 15px; } | |
| #sidebar-sticky-head { top: -14px; padding-top: 14px; } | |
| #sidebar-head .iconbtn { min-width: 38px; height: 38px; } | |
| #page-tree a { padding: 10px 10px; border-radius: 8px; } | |
| #page-tree .tree-caret, #page-tree .tree-caret-spacer { width: 24px; height: 40px; flex-basis: 24px; line-height: 40px; } | |
| #page-tree .group-label { padding: 10px 6px; font-size: 13.5px; } | |
| #page-tree .page-del, #page-tree .group-del { width: 30px; height: 40px; } | |
| #page-tree .page-del, #page-tree .group-del { visibility: visible; opacity: 1; pointer-events: auto; padding: 6px; } | |
| #page-tree .page-del svg, #page-tree .group-del svg { width: 16px; height: 16px; } | |
| /* the desktop edge tabs are replaced by real header buttons */ | |
| #sidebar-toggle, #margin-toggle { display: none; } | |
| #margin-items { padding: 10px 12px calc(18px + env(safe-area-inset-bottom, 0px)); } | |
| .card { padding: 12px 14px; } | |
| /* 16px+ inputs: anything smaller makes iOS Safari zoom in on focus */ | |
| input, textarea, .composer-box textarea, .composer-box input, .reply-row textarea { font-size: 16px; } | |
| #suggest-text { min-height: 30vh; } | |
| /* --- selection menu: thumb-sized --- */ | |
| #selection-menu { border-radius: 10px; } | |
| #selection-menu button { padding: 12px 18px; font-size: 15px; } | |
| /* --- floating bubbles --- */ | |
| #image-bubble, #table-controls { max-width: calc(100vw - 16px); overflow-x: auto; } | |
| #image-bubble .iconbtn, #table-controls .iconbtn { min-width: 34px; height: 34px; } | |
| /* theme + mode controls ride the ⋯ sheet on a phone, where the header is full */ | |
| #more-pop #theme-btn { width: 100%; justify-content: flex-start; gap: 10px; padding: 11px 12px; height: auto; font-size: 14px; } | |
| #more-pop #theme-btn .lbl, #more-pop #docstyle-btn .lbl { display: inline; } | |
| /* the mode switch rides the ⋯ sheet on a phone, where the header is full */ | |
| #more-pop #mode-switch { margin: 4px 8px 6px; justify-content: center; } | |
| #more-pop #mode-switch button { flex: 1; padding: 9px 12px; font-size: 13.5px; } | |
| /* --- home --- */ | |
| .home { padding: 24px 16px 80px; } | |
| .home-head { flex-wrap: wrap; gap: 10px; } | |
| .home h1 { font-size: 20px; } | |
| .home h2 { margin-top: 32px; } | |
| .doc-card { padding: 14px; gap: 10px; } | |
| .doc-card .iconbtn { opacity: 1; min-width: 38px; height: 38px; } | |
| .home #agent-form { flex-wrap: wrap; } | |
| .home #agent-form .btn { padding: 10px 16px; } | |
| .overlay .card { padding: 28px 22px; margin: 0 14px; } | |
| .key-box .row .btn { flex: 1; justify-content: center; } | |
| /* hover tooltips are useless on touch and stick around after a tap */ | |
| [data-tip]:hover::after { display: none; } | |
| } | |
| /* ============================================================ | |
| The document surface, in a window too narrow to hold the page (≤840px). | |
| This block replaces an earlier decision, and the earlier decision deserves | |
| stating because it was not lazy — it was a defensible answer to a real | |
| problem. It read: "The document does NOT get its own phone geometry. Anything | |
| that changed the column here — a fluid sheet, thinner margins, a larger base | |
| size — reflowed the text, so the same document broke its lines differently on | |
| a phone, and resizing a window across the breakpoint moved every line. The | |
| page keeps its width and the column scrolls; use zoom to fit it." | |
| Every clause of that is true. It is also what made a phone unusable, and the | |
| escape hatch it names does not exist. Measured on a 390×844 handset before | |
| this block: the sheet is 816px in a 390px window, so 426px of the 624px text | |
| column — 68% of the line — is off screen, and the 96px page margin spends | |
| 24.6% of the viewport before the first letter. "Use zoom to fit it" means | |
| fitting 816 into 390, i.e. 0.478, which IS inside --doc-zoom's 0.4 floor; but | |
| zoom scales the type with the page, so the body text arrives at 14.667 × 0.478 | |
| = 7.0px. There is no zoom value that both fits the page and leaves a legible | |
| line. That is not a hatch that was hard to find, it is one that is not there. | |
| So the reflow is accepted, deliberately, with its cost named: a document on a | |
| phone breaks its lines somewhere other than where it breaks them on a desktop. | |
| What that buys is a phone that can be read at all, and the two are not | |
| comparable — one is a difference a reader will never see, because they are not | |
| holding the desktop and the phone up side by side, and the other is the | |
| product not working on the device most people have. (Google Docs made the | |
| same trade in the same direction: its mobile | |
| default reflows, and page geometry is the opt-in called "Print layout". We do | |
| not offer that opt-in, because unlike Docs our print path renders its own | |
| #print-view at 18mm/16mm @page margins — so an on-screen 8.5in/1in sheet was | |
| never a faithful proof of the PDF, and previewing pagination is the only thing | |
| print layout on a 390px screen would have been for.) | |
| Why 840px and not the 700px phone breakpoint: the honest question is not "is | |
| this a phone" but "can the page fit", and the page needs 816px plus the 2×8px | |
| the layout keeps as a gutter = 832. 840 gives that a hair of slack. So a | |
| 768px tablet in portrait and an 812px landscape handset reflow — they could | |
| not show a full line either — while an 844px landscape phone, a 900px window | |
| and every desktop keep the page exactly as before, untouched by this block. | |
| It is in two blocks, and the split is the load-bearing part. ≤840 changes only | |
| the *geometry*, and it is deliberately conservative: the sheet goes fluid but | |
| the text column is still held at the 624px the page itself gives, so a 768px | |
| tablet ends up at 624px of measure and 11pt of Arial — the same measure, the | |
| same type, the same line breaks as a desktop, with the horizontal scroll simply | |
| gone. That is worth stating plainly: crossing 840 does not re-break a single | |
| line. Only ≤700 does, where a handset cannot hold 624px at any margin and the | |
| type has to give as well. Reflow is therefore exactly as loud as the screen | |
| forces it to be and no louder. | |
| One case pays both stages at once, and it is a common one: an iPhone 14 is | |
| 844px in landscape — four pixels above the upper breakpoint — so rotating it | |
| crosses 840 AND 700 in a single gesture and re-typesets the document entire. | |
| That is a real cost and it is accepted rather than overlooked. Moving the | |
| breakpoint to 848 to spare it would only hand the same problem to the next | |
| handset, and scroll anchoring holds the reader's place across it (measured: | |
| 421px -> 418px of drift), so what they lose is the line they were on, not the | |
| passage they were in. | |
| No jitter while typing: every length below is a fixed px, a vw, or a | |
| percentage of the sheet. vw does not move when the software keyboard opens | |
| (that is the visual viewport, not the layout one) and does not move when the | |
| URL bar collapses (that is vh). The drawer and the comment sheet are | |
| position:fixed at this width, so opening either one does not resize | |
| #editor-col and does not re-break a line. | |
| ============================================================ */ | |
| @media (max-width: 840px) { | |
| :root { | |
| /* The sheet stops being a page and becomes the column it is on every other | |
| reader: 100% of #editor-col, so it follows the window instead of | |
| overflowing it. Note --doc-zoom drops out of the width here while it stays | |
| in the type, which turns the toolbar's ± from "fit the page" — a job that | |
| no longer exists — into "set the text", the only thing a reader ever | |
| actually wanted from it on a handset. */ | |
| --sheet-w: 100%; | |
| /* Three terms, and the last one is why a tablet does not read worse than a | |
| desktop. (100% - 624px)/2 holds the text column at the 624px the page | |
| itself gives whenever the sheet is wide enough to afford it, so a 752px | |
| sheet takes 64px margins and breaks its lines exactly where a desktop | |
| does. Below ~686px that term goes negative and the other two take over: | |
| 4.5vw is the proportional descendant of the page's 1in (17.6px at 390 — | |
| 1in there would be 24.6% of the screen on *each* side, which is the | |
| operator's complaint in one number), floored at 18px so the suggestion | |
| bar in the margin (left: -9px below) has room to sit beside the text | |
| rather than under the screen edge. | |
| Deliberately NOT multiplied by --doc-zoom: zoom means type size here, and | |
| a reader asking for bigger type is not asking for a narrower measure. */ | |
| --sheet-pad: max(18px, 4.5vw, calc((100% - 624px) / 2)); | |
| /* 11in of blank page below a three-paragraph document is 1056px of nothing | |
| to scroll past. 70vh keeps a tappable sheet under the last line — enough | |
| to place a caret by tapping empty space, which is how you get back into a | |
| short document — without pretending to be a sheet of Letter. */ | |
| --sheet-min-h: 70vh; | |
| } | |
| /* the bar marking a suggested range hangs in the page margin, which is now | |
| 18-64px rather than 96 */ | |
| .tiptap .suggestion-hl::before { left: -9px; } | |
| /* A long unbroken token (a URL) is one way a fluid column still overflows. | |
| Note this block is one of the two that reaches printed output: Chrome | |
| evaluates print media queries against the page box, and Letter is 816px, so | |
| (max-width: 840px) MATCHES in a PDF while (max-width: 700px) does not. That | |
| is the line falling in the right place — the ≤700 type block would be a real | |
| regression in print, since doc.js gives the print sections the tiptap class | |
| — but it does mean this one rule ships to the PDF. Breaking a URL that would | |
| otherwise run off the paper is an improvement there, so it stays. */ | |
| .tiptap :is(p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th) { overflow-wrap: break-word; } | |
| /* The other way is a table someone has dragged a column on. prosemirror-tables | |
| records that drag as inline geometry — style="min-width:561px" on the table | |
| and style="width:513px" on a <col> — which is right on a page 624px wide and | |
| is exactly the sideways pan this file exists to remove once the column is | |
| 354. Measured at 390 before this rule: table 562 inside a 354 measure, 190px | |
| of scroll on #editor-col. It reaches the reader through the stored document, | |
| so a drag on a desktop follows them onto the handset. | |
| Both declarations are load-bearing — dropping either leaves 189/190px of the | |
| scroll behind, because the <table> min-width and the <col> width pin the | |
| geometry independently. !important is not a shortcut here but the only | |
| instrument that reaches an inline style: no selector, at any specificity, | |
| outranks a style attribute. Confined to the reflow widths, so the drag still | |
| means what the reader meant by it everywhere the page is a page. */ | |
| .tiptap table { min-width: 0 ; } | |
| .tiptap table col { width: auto ; } | |
| } | |
| /* --- and the type, on a handset (≤700px) ------------------------------------- | |
| Everything above is geometry. Below 700px the 624px measure is not available | |
| at any margin, so the column is ~354px and the document's own type sizes — | |
| all of them chosen against a 624px column on a page — have to be re-derived. | |
| This is the part the old comment called out as "a larger base size", and it is | |
| the part that genuinely moves the words. It is scoped to a handset for that | |
| reason. */ | |
| @media (max-width: 700px) { | |
| /* 11pt Arial is a page size, and the page it is sized for is 8.5in wide. In a | |
| 354px column it is both too small to read on a handset — iOS sets its own | |
| body text at 17px, Android at 16 — and, at 48 characters, more of a line | |
| than a phone should be asked to track. 16px gives 44 characters / 7.8 words | |
| a line, inside the 35-45 characters phone readers settle on, and it is the | |
| same 16px iOS uses as its own don't-zoom-on-focus threshold, which the | |
| inputs in the phone block above are already pinned to. Headings, list | |
| indents and rules are all em off this base, so they follow. */ | |
| :root { --doc-size: 16px; } | |
| /* Reading's base is already 16px, and its measure cannot be saved: the style | |
| was tuned to 10.69 words a line in a 624px column and lands at 6.7 in 354px. | |
| That is not a compromise so much as what a phone is — the magazine's own app | |
| runs 6 to 8. So the base is left exactly where it was measured and only the | |
| leading gives: 1.8 was picked for a long line, and 28.8px of air between | |
| 42-character lines reads gappy rather than open. 1.62 keeps Reading clearly | |
| the airier of the two styles against Docs' 1.15 and puts ~1.5 more lines on | |
| a screenful. */ | |
| :root[data-doc-style="reading"] { --doc-line: 1.62; } | |
| /* A drop shadow describes a sheet lying on a canvas. There is no canvas left | |
| at this width — #layout drops its gutter in the phone block above and the | |
| sheet is the screen — so the shadow has nothing to cast onto and only | |
| clipped at the edges. Between 701 and 840 the 8px gutter is still there and | |
| the sheet still reads as a sheet, so it keeps its shadow. */ | |
| #editor, #doc-skeleton { box-shadow: none; } | |
| /* Display type does not scale with the column on its own: the Docs Title is | |
| 2.364em and Reading's headline 2.909em, both proportions struck against a | |
| 624px measure. At 354px that is 37.8px and 46.5px — five and six lines for | |
| an ordinary title, a screenful of headline before the article starts. These | |
| are the same proportions re-struck for the shorter measure, at a title of | |
| two to three lines; Reading keeps the larger of the two because a display | |
| serif at text size stops being a display serif. */ | |
| .tiptap > h1:first-child { font-size: 1.75em; } | |
| .tiptap h1 { font-size: 1.5em; } | |
| [data-doc-style="reading"] .tiptap > h1:first-child { font-size: 1.95em; line-height: 1.18; } | |
| [data-doc-style="reading"] .tiptap h1 { font-size: 1.95em; line-height: 1.18; } | |
| /* 1.6em of small caps with 0.075em of tracking is 25.6px of letterspaced | |
| capitals — a section head that wrapped mid-phrase in a 354px column */ | |
| [data-doc-style="reading"] .tiptap h2, | |
| [data-doc-style="reading"] .tiptap h3 { font-size: 1.3em; } | |
| /* Docs indents list text to a half-inch tab stop. 36pt of a 354px column is a | |
| tenth of the measure spent on the marker's gutter. */ | |
| .tiptap ul, .tiptap ol { padding-left: 24pt; } | |
| .tiptap blockquote { padding-left: 12pt; } | |
| /* Tables stay width:100% with table-layout:fixed, so they divide the measure | |
| rather than overflow it — three columns get ~118px each, tight but wrapping. | |
| That holds for a table nobody has touched. A table whose columns have been | |
| dragged carries an inline min-width that overrides all of it, which is what | |
| the two !important rules in the ≤840 block above are for; this comment used | |
| to claim there was no min-width to worry about, and it was wrong. */ | |
| } | |
| @media (hover: none) { | |
| [data-tip]:hover::after { display: none; } | |
| } | |
| /* --- link bubble ------------------------------------------------------------- | |
| Raised by a plain click on a link: names where the link goes and gives a | |
| mouse something to press. Without it, following a cross-reference needed a | |
| ctrl/cmd+click that nothing on screen mentioned. */ | |
| #link-bubble { | |
| position: fixed; z-index: 70; display: flex; align-items: center; gap: 6px; | |
| background: var(--surface); border: 1px solid var(--border); border-radius: 9px; | |
| box-shadow: var(--shadow-md); padding: 5px 6px; font-family: var(--ui); | |
| } | |
| #link-bubble.hidden { display: none; } | |
| #link-bubble .lb-target { | |
| max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |
| border: none; background: none; padding: 2px 4px; font-size: 12.5px; | |
| color: var(--text-2); cursor: pointer; font-family: inherit; | |
| } | |
| #link-bubble .lb-target.internal { color: var(--accent); } | |
| #link-bubble .lb-target:hover { text-decoration: underline; } | |
| #link-bubble .btn.small { padding: 3px 10px; font-size: 12.5px; } | |
| #link-bubble .iconbtn { padding: 4px; } | |
| /* --- printing (the PDF path) ------------------------------------------------- | |
| #print-view holds a plain rendition of the chosen pages. On screen it is | |
| hidden; on paper it is the ONLY thing, so the app chrome, margins and editor | |
| decorations never reach the page. */ | |
| #print-view { display: none; } | |
| @media print { | |
| body.printing > *:not(#print-view) { display: none ; } | |
| body.printing { background: #fff; } | |
| #print-view { display: block; } | |
| /* paper is the page box, so the sheet itself carries no chrome — but it keeps | |
| the document's own typography: same faces, same scale, same rhythm. | |
| --doc-size bare, without --doc-text-scale: that scale is one reader's | |
| comfort setting held in one browser's localStorage, and a PDF is a thing | |
| handed to other people. Nobody else's copy should come out set in 22pt | |
| because whoever exported it reads large. Zoom is left out for the same | |
| reason, and always has been. */ | |
| #print-view .print-page { | |
| max-width: none; padding: 0; background: #fff; | |
| font-family: var(--doc); font-size: var(--doc-size); line-height: var(--doc-line); | |
| color: #000; | |
| } | |
| #print-view .print-page > * + * { margin-top: 0; } | |
| #print-view .print-page.page-break { break-before: page; page-break-before: always; } | |
| /* a heading should not be the last line on a sheet, nor a figure be split */ | |
| #print-view h1, #print-view h2, #print-view h3 { break-after: avoid; page-break-after: avoid; } | |
| #print-view img, #print-view table, #print-view pre, #print-view blockquote { break-inside: avoid; page-break-inside: avoid; } | |
| #print-view img { max-width: 100%; height: auto; } | |
| #print-view a { color: #000; text-decoration: underline; } | |
| @page { margin: 18mm 16mm; } | |
| } | |
| /* --- public link visitors -------------------------------------------------- | |
| The socket already refuses their writes; this keeps the UI from offering | |
| actions that would silently do nothing. Reading — pages, comments, zoom, | |
| theme, print — is left alone. */ | |
| body.viewer #share-btn, | |
| body.viewer #agents-btn, | |
| body.viewer #docs-btn, | |
| body.viewer #download-btn, | |
| body.viewer #mode-switch, | |
| body.viewer #suggest-btn, | |
| body.viewer #header-tools, | |
| body.viewer #composer, | |
| body.viewer #suggest-box, | |
| body.viewer .card .accept-btn, | |
| body.viewer .card .reject-btn, | |
| body.viewer .card .close-btn, | |
| body.viewer .card .reply, | |
| /* #hover-comment arrives with the position-comment PR (#15); harmless until | |
| then, and it must not offer a stranger a comment box the socket will refuse */ | |
| body.viewer #hover-comment, | |
| body.viewer #page-add, | |
| body.viewer #group-add, | |
| body.viewer .margin-add, | |
| /* Hocuspocus sends awareness to every connection, read-only included, so this | |
| hides the presence row rather than withholding it — the same limit as the | |
| comments a link publishes. Worth doing anyway: a stranger should not be shown | |
| a row of collaborators, and nothing here invites them to think they are one. */ | |
| body.viewer #presence { display: none ; } | |
| body.viewer .tiptap { caret-color: transparent; } | |
| /* the "anyone with the link" half of the Share dialog */ | |
| #link-share { border-top: 1px solid var(--line); margin-top: 10px; padding-top: 10px; } | |
| #link-share .link-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; } | |
| #link-toggle { white-space: nowrap; } | |
| #link-share .hint { margin: 2px 0 0; } | |
| #link-row { display: flex; gap: 6px; margin-top: 8px; } | |
| #link-row input { flex: 1; font-size: 12px; font-family: var(--mono, monospace); } | |