@molecule/app-margin-notes-html
Feature · margin-notes · App (browser) · v1.0.8 · Apache-2.0
Framework-free long-form reading layout with notes beside each paragraph, rendered as an HTML string (build time or server) plus a small browser script: aligned sticky notes column, margin marks, two-way hover, switchable note kinds, and a phone bar that follows the reader
npm install @molecule/app-margin-notes-htmlHow it works
@molecule/app-margin-notes-html is a ready-made margin-notes feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
// A static-site build step: one post page, notes from provenance + summaries.
import {
marginNotesCss,
marginNotesScriptTag,
renderMarginNotes,
type MarginNote,
type MarginNotesBlock,
} from '@molecule/app-margin-notes-html'
interface Span {
text: string
origin: 'human' | 'ai'
prompt?: string
model?: string
}
// spans: one per top-level markdown block, in page order (e.g. provenance.json).
// summaries: TL;DR text keyed by the index of the span that starts each section.
// renderBlock: your markdown renderer, one block → HTML.
export function postBody(
spans: Span[],
summaries: Record<number, string>,
renderBlock: (markdown: string) => string,
): { body: string; switches: string } {
const notes: MarginNote[] = []
const promptIds = new Map<string, string>()
let summaryId: string | undefined
const blocks: MarginNotesBlock[] = spans.map((span, i) => {
if (/^#{1,6}\s/.test(span.text) || i === 0) {
// A new section: its summary covers every block until the next heading.
summaryId = summaries[i] ? `summary-${i}` : undefined
if (summaryId)
notes.push({
id: summaryId,
kind: 'summary',
label: 'TL;DR',
html: `<p>${summaries[i]}</p>`,
})
}
const noteIds = summaryId ? [summaryId] : []
if (span.origin === 'ai' && span.prompt) {
let id = promptIds.get(span.prompt)
if (!id) {
id = `prompt-${promptIds.size}`
promptIds.set(span.prompt, id)
notes.push({
id,
kind: 'prompt',
label: `Prompt · ${span.model ?? ''}`,
html: `<p>${span.prompt}</p>`,
})
}
noteIds.push(id)
}
return { id: `block-${i}`, html: renderBlock(span.text), noteIds, marked: span.origin === 'ai' }
})
const { html, switchesHtml } = renderMarginNotes({
blocks,
notes,
kinds: [
{ id: 'summary', label: 'Summaries', defaultOn: true },
{ id: 'prompt', label: 'Prompts', defaultOn: false, panel: 'tap' },
],
markLabel: 'Written with AI',
})
return { body: html, switches: switchesHtml }
}
// In the page template: the switches under the title, the body, then the
// stylesheet (once, BEFORE your site CSS so your own rules win a tie) and
// the script (once, end of <body>):
// <header><h1>…</h1>${switches}</header>${body}${marginNotesScriptTag()}
// fs.writeFileSync('dist/styles.css', marginNotesCss + siteCss)Works with: @molecule/app-i18n
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
Long-form reading layout with notes beside each paragraph, as plain HTML — for static sites and server-rendered pages that have no React.
renderMarginNotes() turns your blocks (paragraphs, headings) and notes
(summaries, prompts, citations…) into HTML: on a wide screen a prose column
with a narrower notes column beside it, each note lined up with the block it
covers and sticky while its section scrolls past; on a phone a bar fixed to
the bottom with the switches and a panel that shows the notes for the
section being read. marginNotesCss styles it and marginNotesScript makes
it interactive (switches, two-way hover, the phone panel following the
reader, tap to pin). Defaults are in the HTML, so the page reads correctly
with no JavaScript. The same layout for React apps is
@molecule/app-margin-notes-react.
Quick Start
// A static-site build step: one post page, notes from provenance + summaries.
import {
marginNotesCss,
marginNotesScriptTag,
renderMarginNotes,
type MarginNote,
type MarginNotesBlock,
} from '@molecule/app-margin-notes-html'
interface Span {
text: string
origin: 'human' | 'ai'
prompt?: string
model?: string
}
// spans: one per top-level markdown block, in page order (e.g. provenance.json).
// summaries: TL;DR text keyed by the index of the span that starts each section.
// renderBlock: your markdown renderer, one block → HTML.
export function postBody(
spans: Span[],
summaries: Record<number, string>,
renderBlock: (markdown: string) => string,
): { body: string; switches: string } {
const notes: MarginNote[] = []
const promptIds = new Map<string, string>()
let summaryId: string | undefined
const blocks: MarginNotesBlock[] = spans.map((span, i) => {
if (/^#{1,6}\s/.test(span.text) || i === 0) {
// A new section: its summary covers every block until the next heading.
summaryId = summaries[i] ? `summary-${i}` : undefined
if (summaryId)
notes.push({
id: summaryId,
kind: 'summary',
label: 'TL;DR',
html: `<p>${summaries[i]}</p>`,
})
}
const noteIds = summaryId ? [summaryId] : []
if (span.origin === 'ai' && span.prompt) {
let id = promptIds.get(span.prompt)
if (!id) {
id = `prompt-${promptIds.size}`
promptIds.set(span.prompt, id)
notes.push({
id,
kind: 'prompt',
label: `Prompt · ${span.model ?? ''}`,
html: `<p>${span.prompt}</p>`,
})
}
noteIds.push(id)
}
return { id: `block-${i}`, html: renderBlock(span.text), noteIds, marked: span.origin === 'ai' }
})
const { html, switchesHtml } = renderMarginNotes({
blocks,
notes,
kinds: [
{ id: 'summary', label: 'Summaries', defaultOn: true },
{ id: 'prompt', label: 'Prompts', defaultOn: false, panel: 'tap' },
],
markLabel: 'Written with AI',
})
return { body: html, switches: switchesHtml }
}
// In the page template: the switches under the title, the body, then the
// stylesheet (once, BEFORE your site CSS so your own rules win a tie) and
// the script (once, end of <body>):
// <header><h1>…</h1>${switches}</header>${body}${marginNotesScriptTag()}
// fs.writeFileSync('dist/styles.css', marginNotesCss + siteCss)
Type
feature
Installation
npm install @molecule/app-margin-notes-html @molecule/app-i18n
API
Interfaces
MarginNote
A note shown beside the block(s) it belongs to.
interface MarginNote {
/** Stable id, referenced from `MarginNotesBlock.noteIds`. */
id: string
/** The kind this note belongs to (`MarginNoteKind.id`), for its switch. */
kind: string
/**
* A small label above the note, already translated (e.g. "TL;DR" or
* "Prompt · claude-fable-5"). Plain text; escaped.
*/
label?: string
/** The note's HTML. Trusted: it is inserted as is. */
html: string
}
MarginNoteKind
A kind of note the reader can show or hide with a switch.
interface MarginNoteKind {
/** Stable id, referenced from `MarginNote.kind`. */
id: string
/** The switch's visible label, already translated (e.g. "Summaries"). */
label: string
/** Whether this kind shows before the reader touches its switch. Default `true`. */
defaultOn?: boolean
/**
* How this kind reaches the phone panel. `'follow'` (default): the panel
* shows it for the section being read, following the reader. `'tap'`: for
* notes about one paragraph rather than the section — while its switch is
* OFF, only a tap on a block it belongs to shows it (gone on the second tap);
* while its switch is ON it follows the reader like `'follow'`, so turning
* the switch on always shows something. A tap shows the note either way.
*/
panel?: 'follow' | 'tap'
/**
* The kind's accent colour (any CSS colour). Notes of different kinds are
* told apart by colour alone: same font, same size. Defaults to
* `var(--mn-accent-1)`, `var(--mn-accent-2)`, … by position.
*/
accent?: string
}
MarginNotesBlock
One block of the text — usually a paragraph or heading from your markdown renderer. Blocks render in order, in the prose column.
interface MarginNotesBlock {
/** Stable id, unique within the page (used as the DOM anchor). */
id: string
/** The block's HTML (e.g. `<p>…</p>`). Trusted: it is inserted as is. */
html: string
/** Ids of the notes that belong to this block (see `MarginNote`). */
noteIds?: string[]
/**
* Draw a small mark in this block's left margin — the one permanent sign on
* the prose itself (e.g. "written with AI"). Unmarked blocks carry none.
*/
marked?: boolean
}
MarginNotesRow
One row of the layout: a run of blocks and the notes shown beside it.
interface MarginNotesRow {
/** The row's key: the id of its first block. */
id: string
/** The blocks in this row, in order. */
blocks: MarginNotesBlock[]
/** Notes shown beside this row — each note appears in exactly one row, its first. */
notes: MarginNote[]
/** Every note id any block in this row refers to (for the phone panel). */
noteIds: string[]
}
RenderedMarginNotes
What renderMarginNotes returns.
interface RenderedMarginNotes {
/** The layout: prose, gutter, and the phone bar (with its own switches). */
html: string
/**
* The desktop switches, for you to place (e.g. centred under the title).
* `''` when there is nothing to switch. Hidden at phone width, where the
* bar's switches replace them.
*/
switchesHtml: string
/** Whether any block has a note (when `false`, `html` is plain centred prose). */
hasNotes: boolean
}
RenderMarginNotesOptions
Options for renderMarginNotes.
interface RenderMarginNotesOptions {
/** The text, in reading order. */
blocks: MarginNotesBlock[]
/** Every note the blocks refer to. A page with none renders centred prose: no gutter, no bar, no switches. */
notes?: MarginNote[]
/** The note kinds, in the order their switches appear. Kinds absent here are always shown. */
kinds?: MarginNoteKind[]
/** Accessible name of the margin mark, already translated (e.g. "Written with AI"). */
markLabel?: string
/**
* Id of this layout. Switches rendered elsewhere on the page (e.g. under the
* title) find their layout by it. Default `'margin-notes'`.
*/
id?: string
}
Functions
attachMarginNotes(doc)
Makes every rendered layout on the page interactive: the switches show and hide their kind (desktop and phone switches stay in step), hovering or focusing a block emphasises its notes and hovering a note tints the blocks it covers, notes line up with the first line they cover, and on a phone the bottom panel follows the section being read and a tap on a block pins its notes (a second tap, or "Hide notes", lets go). Safe to call more than once.
This function is self-contained (no imports, no module state), so
marginNotesScript ships it to the browser as a plain string.
function attachMarginNotes(doc?: Document): void
doc— The document to attach to (default: the globaldocument).
buildRows(blocks, notes, kinds)
Group blocks into layout rows and place each note once.
A row runs for as long as its blocks are covered by a note it OPENED with: a section's summary (every block of the section refers to it) keeps the whole section in one row, and a note that first appears mid-row — a prompt behind one of the section's paragraphs — joins that row's gutter but does not extend the row. (A prompt that produced paragraphs in several sections once carried a row across the next heading, and that section's summary was drawn beside the previous section.) A sticky note is bounded by its row, so this is what keeps a summary pinned until its section ends; splitting the row wherever the note set changed released it after the first paragraph. A run of blocks with no notes is a row of its own. Each note is shown once, in the first row that refers to it.
Only a note about the whole section keeps a row going: a note of a 'tap'
kind is about one paragraph, so it never carries a row past its block. Before
that, a prompt that wrote a post's opening AND the next section's first
paragraph opened the first row with the summary, and the next section's
summary was stacked under it, 180–200 px above its own heading (X0 x375,
x376, 2026-09-29).
function buildRows(
blocks: MarginNotesBlock[],
notes?: MarginNote[],
kinds?: MarginNoteKind[],
): MarginNotesRow[]
blocks— The text, in reading order.notes— Every note the blocks refer to; ids with no note are ignored.kinds— The note kinds; a'tap'kind never extends a row. Without them every note does.
Returns: The rows, in reading order.
defaultShownKinds(kinds)
The kinds shown before the reader touches a switch.
function defaultShownKinds(kinds?: { id: string; defaultOn?: boolean }[]): string[]
kinds— The switchable kinds.
Returns: The ids of the kinds that default on.
escapeHtml(s)
Escapes text for use in HTML text and attribute values.
function escapeHtml(s: string): string
s— Plain text.
Returns: The escaped text.
marginNotesScriptTag()
The browser script as a <script> element.
function marginNotesScriptTag(): string
Returns: <script>…</script>.
marginNotesStyleTag()
The stylesheet as a <style> element, for pages that inline it.
function marginNotesStyleTag(): string
Returns: <style>…</style>.
mergeRepeatedNotes(blocks, notes)
One note per act of writing: when consecutive blocks each carry their own
note with the same kind, label and HTML (one prompt that produced three
paragraphs, recorded as three notes), every later block is pointed at the
first block's note instead. buildRows places each note once, so the
prompt is shown once, beside the first paragraph it produced, and hovering
any of those paragraphs highlights that one note. The same text appearing
again after a block without it is a new act, and keeps its own note.
function mergeRepeatedNotes(blocks: MarginNotesBlock[], notes?: MarginNote[]): MarginNotesBlock[]
blocks— The text, in reading order.notes— Every note the blocks refer to.
Returns: The blocks, with repeated notes pointed at the first copy.
renderMarginNotes(options)
Renders a long-form text with notes beside each paragraph, as HTML.
Desktop: the prose column and, beside it, a narrower notes column; each
note lines up with the block(s) it belongs to, appears once, and stays in
view while its section scrolls past. Phone: the notes column disappears and
a bar fixed to the bottom carries the switches and a panel with the notes
for the section being read. Every default — which kinds show, which surface
a width gets — is in the HTML and the CSS, so the page is right before any
script runs; marginNotesScript adds the switches, hover, and the
phone panel's following.
function renderMarginNotes(options: RenderMarginNotesOptions): RenderedMarginNotes
options— SeeRenderMarginNotesOptions.
Returns: The layout, the desktop switches to place, and whether it has notes.
Constants
marginNotesCss
The layout's stylesheet. It selects only on the data-mn-* attributes the
renderer writes (no class names), and every size and colour is a custom
property you can override on :root or on the layout:
| property | default | what |
|---|---|---|
--mn-measure | 36rem | prose column width |
--mn-gutter | 18rem | notes column width |
--mn-gap | 2.5rem | space between the columns |
--mn-prose-size / --mn-prose-size-phone | 1.25rem / 1.125rem | prose font size |
--mn-line-height | 1.6 | prose line height |
--mn-note-scale | 0.75 | note size ÷ prose size (desktop) |
--mn-panel-scale | 0.8 | note size ÷ prose size (phone panel) |
--mn-accent-1, --mn-accent-2, … | blue, amber | each kind's colour, by position |
--mn-sticky-top | 1rem | how far below the viewport top a note sticks (your header's height) |
--mn-surface | Canvas | the phone bar's background |
--mn-breakpoint | — | fixed at 768px (media queries cannot read custom properties) |
const marginNotesCss: '\n:where(:root) {\n --mn-measure: 36rem;\n --mn-gutter: 18rem;\n --mn-gap: 2.5rem;\n --mn-prose-size: 1.25rem;\n --mn-prose-size-phone: 1.125rem;\n --mn-line-height: 1.6;\n --mn-note-scale: 0.75;\n --mn-panel-scale: 0.8;\n --mn-accent-1: #2563eb;\n --mn-accent-2: #b45309;\n --mn-accent-3: #047857;\n --mn-sticky-top: 1rem;\n --mn-surface: Canvas;\n}\n@media (prefers-color-scheme: dark) {\n :where(:root) { --mn-accent-1: #60a5fa; --mn-accent-2: #f59e0b; --mn-accent-3: #34d399; }\n}\n[data-mn-root] {\n box-sizing: border-box;\n max-width: var(--mn-measure);\n margin-inline: auto;\n font-size: var(--mn-prose-size);\n line-height: var(--mn-line-height);\n}\n[data-mn-root][data-mn-has-notes] { max-width: calc(var(--mn-measure) + var(--mn-gap) + var(--mn-gutter)); }\n[data-mn-rows] > [data-mn-row]:first-child [data-mn-block]:first-child > :first-child { margin-top: 0; }\n[data-mn-block] { position: relative; border-radius: 0.25rem; transition: background-color 0.15s; }\n[data-mn-block][data-mn-notes] { cursor: pointer; }\n[data-mn-block][data-mn-tint] {\n background: color-mix(in srgb, var(--mn-tint-color, var(--mn-accent-1)) 10%, transparent);\n}\n[data-mn-block][data-mn-notes]:focus-visible { outline: 2px solid var(--mn-accent-1); outline-offset: 2px; }\n[data-mn-mark] {\n position: absolute; left: -0.9rem; top: 0.75em;\n width: 6px; height: 6px; border-radius: 50%;\n background: var(--mn-accent-1); display: none;\n}\n[data-mn-gutter] { display: none; }\n[data-mn-sticky] {\n position: sticky; top: var(--mn-sticky-top);\n display: flex; flex-direction: column; gap: 0.75rem;\n margin-top: var(--mn-offset, 0px);\n}\n[data-mn-note] {\n position: relative;\n box-sizing: border-box;\n font-family: inherit;\n font-size: calc(var(--mn-prose-size) * var(--mn-note-scale));\n line-height: 1.5;\n padding: 0.625rem 0.75rem 0.625rem 1.5rem;\n border-radius: 0.375rem;\n background: color-mix(in srgb, var(--mn-note-accent, var(--mn-accent-1)) 7%, transparent);\n transition: background-color 0.15s, box-shadow 0.15s;\n}\n/* The accent bar sits inside the note, clear of its rounded corners, and the\n label carries the same color (the fleet\'s accent-bar treatment). */\n[data-mn-note]::before {\n content: \'\';\n position: absolute;\n left: 0.625rem;\n top: 0.75rem;\n bottom: 0.75rem;\n width: 4px;\n border-radius: 2px;\n background: var(--mn-note-accent, var(--mn-accent-1));\n pointer-events: none;\n}\n[data-mn-note][hidden] { display: none !important; }\n[data-mn-note][data-mn-emphasis] {\n background: color-mix(in srgb, var(--mn-note-accent, var(--mn-accent-1)) 16%, transparent);\n box-shadow: 0 1px 4px rgb(0 0 0 / 0.14);\n}\n[data-mn-note]:focus-visible { outline: 2px solid var(--mn-note-accent, var(--mn-accent-1)); outline-offset: 2px; }\n[data-mn-note-label] {\n margin: 0 0 0.25rem;\n font-size: 0.75em; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--mn-note-accent, var(--mn-accent-1));\n}\n[data-mn-note-body] > :first-child { margin-top: 0; }\n[data-mn-note-body] > :last-child { margin-bottom: 0; }\n[data-mn-switches] {\n display: flex; flex-wrap: nowrap; align-items: center; justify-content: center; gap: 1rem;\n}\n[data-mn-switches="side"] { display: none; }\n[data-mn-switch] {\n position: relative;\n display: inline-flex; align-items: center; gap: 0.5rem;\n min-height: 44px; padding: 0 0.25rem;\n background: none; border: 0; color: inherit;\n font: inherit; font-size: 0.9rem; white-space: nowrap; cursor: pointer;\n}\n[data-mn-switch]:focus-visible { outline: 2px solid var(--mn-note-accent, var(--mn-accent-1)); outline-offset: 2px; border-radius: 0.375rem; }\n[data-mn-track] {\n position: relative; flex-shrink: 0;\n width: 40px; height: 22px; border-radius: 999px;\n background: color-mix(in srgb, currentColor 22%, transparent);\n transition: background-color 0.15s;\n}\n[data-mn-switch][aria-checked="true"] [data-mn-track] { background: var(--mn-note-accent, var(--mn-accent-1)); }\n[data-mn-knob] {\n position: absolute; top: 2px; left: 2px;\n width: 18px; height: 18px; border-radius: 50%;\n background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / 0.3);\n transition: transform 0.15s;\n}\n[data-mn-switch][aria-checked="true"] [data-mn-knob] { transform: translateX(18px); }\n[data-mn-bar] {\n position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;\n box-sizing: border-box;\n padding: 0.25rem 0.75rem calc(0.25rem + env(safe-area-inset-bottom, 0px));\n background: var(--mn-surface);\n border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent);\n box-shadow: 0 -2px 10px rgb(0 0 0 / 0.08);\n font-size: var(--mn-prose-size-phone);\n}\n[data-mn-panel] {\n max-height: 0; overflow: hidden;\n transition: max-height 0.2s ease;\n}\n[data-mn-panel][data-mn-open] { max-height: 45vh; overflow-y: auto; padding-top: 0.5rem; }\n[data-mn-panel] [data-mn-note] {\n font-size: calc(var(--mn-prose-size-phone) * var(--mn-panel-scale));\n margin-bottom: 0.5rem;\n}\n[data-mn-dismiss] {\n min-height: 44px; padding: 0 0.25rem;\n background: none; border: 0; color: inherit;\n font: inherit; font-size: 0.85rem; text-decoration: underline; cursor: pointer;\n}\n[data-mn-dismiss][hidden] { display: none; }\n@media (max-width: 767.98px) {\n [data-mn-root] { font-size: var(--mn-prose-size-phone); }\n /* The bar is fixed to the viewport, so it covers the END OF THE PAGE, not just\n the article: reserve its height after everything the page shows. */\n html:has([data-mn-root][data-mn-has-notes] [data-mn-bar]) body::after {\n content: \'\'; display: block; height: var(--mn-bar-space, 9rem);\n }\n}\n@media (min-width: 768px) {\n [data-mn-root][data-mn-has-notes] [data-mn-row] {\n display: grid;\n grid-template-columns: minmax(0, var(--mn-measure)) var(--mn-gutter);\n column-gap: var(--mn-gap);\n }\n [data-mn-gutter] { display: block; }\n [data-mn-mark] { display: block; }\n [data-mn-switches="side"] { display: flex; }\n [data-mn-bar] { display: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-mn-panel], [data-mn-knob], [data-mn-track], [data-mn-note], [data-mn-block] { transition: none; }\n}\n'
marginNotesScript
The browser script: attachMarginNotes as a string that runs itself.
Put it in the page once (see marginNotesScriptTag) or in your own
bundle; it attaches to every layout on the page when it runs, so place it
after the layout (end of <body>), or give the tag defer.
const marginNotesScript: string
Injection Notes
Requirements
Peer dependencies:
@molecule/app-i18n^1.0.1
Runtime Dependencies
-
@molecule/app-i18n -
Do NOT hand-build any of it — no grid of your own, no hover, scroll or IntersectionObserver script, no margin dots, no bottom bar, no switch markup or toggle state.
renderMarginNotes+marginNotesCss+marginNotesScriptare all of it. -
Put
switchesHtmlwhere the page's toggles go (e.g. centred under the title). It is''when no block has a note — then there is no gutter, no bar and nothing to toggle, and the prose is centred at the reading measure. At phone width the side switches are hidden and the bar's switches are the only ones: never render a second set yourself. -
Defaults live in
kinds[].defaultOn, not in CSS or a script: a default-off kind's notes are rendered with thehiddenattribute and its switches readaria-checked="false"in the HTML. -
One block per top-level markdown block (paragraph, heading, list, code), each with a stable
id; ONE note per prompt or per section summary, its id in thenoteIdsof every block it covers — it shows once, beside the first. Consecutive blocks with the samenoteIdsform one row. -
Kinds are told apart by colour alone (same font, same size): each kind gets
--mn-accent-<position>or its ownaccent. A note'slabelis a small uppercase line above it (e.g.TL;DR,Prompt · <model>). -
htmlon blocks and notes is inserted as is — pass HTML from your own renderer, and escape any plain text you put in it (escapeHtml). -
Style by overriding the
--mn-*custom properties (seemarginNotesCss): measure, gutter, prose size, note scale, accents, and--mn-sticky-top(set it to your sticky header's height). The stylesheet selects ondata-mn-*attributes only. -
renderMarginNotesruns anywhere (Node at build time, a server, a browser); the script needs a browser. UI strings (switch group, notes landmark, phone panel, "Hide notes") come from@molecule/app-i18nwith the keys of@molecule/app-locales-margin-notes; kind labels, note labels andmarkLabelare yours to translate. -
One layout per page by default; give each an
idif you render more. -
Order the stylesheet BEFORE your site's CSS. Its rules are plain attribute selectors, so whichever sheet comes last wins a tie: put
marginNotesCssfirst and your overrides (tokens or rules) after it. -
Selectors for tests — every piece carries a
data-mol-id(placement issidefor the desktop switches,barfor the phone bar):Element data-mol-idalso the layout root margin-notes[data-mn-root]a switch group margin-notes-switches-<placement>[data-mn-switches]one switch (the button to click) margin-notes-switch-<placement>-<kind>[data-mn-switch][data-mn-kind=<kind>],aria-checkeda row margin-notes-row-<blockId>[data-mn-row]a block margin-notes-block-<blockId>[data-mn-block],[data-mn-notes]a note margin-note-<gutter|panel>-<noteId>[data-mn-note],hiddenwhen offthe phone bar / its panel margin-notes-bar/margin-notes-panel[data-mn-open]when showing notes"Hide notes" margin-notes-dismissshown while a block is pinned At phone width the
sideswitches are hidden: click thebarones.