/* CM6 Live Preview Editor Styles */ @import '../components/external-link.css'; /* ---- Editor container ---- */ .cm6-editor-container { height: 100%; width: 100%; overflow: hidden; } .cm6-editor-container .cm-editor { height: 100%; } .cm6-editor-container .cm-scroller { overflow: auto; scrollbar-width: thin; scrollbar-color: var(--text-tertiary) var(--bg-primary); /* Chromium stops touch-action negotiation at the nearest scroll container. * The shell's pan-y rule alone cannot keep horizontal pointerup alive here. */ touch-action: pan-y; font-family: var(--font-text, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); font-size: var(--font-size-text, 16px); line-height: var(--line-height-text, 1.6); padding: 1rem; overscroll-behavior-y: none; } /* Older Chromium builds use WebKit scrollbar styling instead of scrollbar-width. */ .cm6-editor-container .cm-scroller::-webkit-scrollbar { width: 8px; height: 8px; } @media (max-width: 767px) { .cm6-editor-container .cm-scroller { padding-inline: 1.5rem; } } .cm6-editor-container .cm-content { max-width: 42rem; margin: 0 auto; } /* Type Records are an opaque, contenteditable=false block widget. The * surrounding markdown remains the selection/navigation surface. */ .cm-block-record-sections { container-type: inline-size; box-sizing: border-box; margin: 2px auto; max-width: min(100%, 38rem); padding: 2px 0.5rem 0.45rem; border: 1px solid color-mix(in srgb, var(--text-secondary) 25%, transparent); border-radius: 2px; background: var(--background-secondary); } .cm-block-record-sections .reams-record-section + .reams-record-section { margin-top: 0.35rem; } .cm-root-frontmatter-surface { --reams-frontmatter-cell-padding: 0.35rem; --reams-frontmatter-control-height: calc(1.6rem + 2px); --reams-frontmatter-value-inset: calc(0.35rem + 1px); display: grid; width: 100%; max-width: 100%; margin: 0 0 1rem; padding: 0; border: 0; border-radius: 0; background: transparent; gap: 0.3rem; margin-bottom: 1rem; overflow: hidden; } .cm-root-frontmatter-surface .reams-record-fields label, .cm-root-frontmatter-surface .reams-record-fields input:not([type="checkbox"]), .cm-root-frontmatter-surface .reams-record-fields select, .cm-root-frontmatter-surface .reams-record-fields textarea, .cm-root-frontmatter-surface .reams-record-fields fieldset { min-height: 1.75rem; } .reams-root-types, .reams-yaml-section { min-width: 0; } .reams-root-types { display: grid; grid-template-columns: 8rem minmax(0, 1fr); align-items: start; gap: 0.2rem; } .reams-root-section-label { padding: 0 var(--reams-frontmatter-cell-padding); color: var(--text-secondary); font-size: 0.875em; font-weight: 400; } .reams-root-type-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; } .reams-root-type-chips .reams-record-type-chip { gap: 0.2rem; } .reams-root-type-chips .reams-record-type-chip button, .reams-root-type-chips > button, .reams-yaml-field > button, .reams-yaml-sequence-item > button, .reams-yaml-sequence > button, .reams-yaml-add, .reams-yaml-add-form button, .reams-root-add, .reams-root-add-type-form button { min-width: 1.75rem; min-height: 1.75rem; padding: 0 0.25rem; border: 0; border-radius: 2px; background: transparent; color: var(--text-tertiary); cursor: pointer; font: inherit; } .reams-root-type-chips .reams-record-type-chip button:hover, .reams-root-type-chips > button:hover, .reams-yaml-field > button:hover, .reams-yaml-sequence-item > button:hover, .reams-yaml-sequence > button:hover, .reams-yaml-add:hover, .reams-yaml-add-form button:hover, .reams-root-add:hover, .reams-root-add-type-form button:hover { background: var(--bg-hover); color: var(--text-primary); } .reams-root-type-chips select, .reams-yaml-field input, .reams-yaml-field select, .reams-yaml-field textarea, .reams-yaml-add-form input, .reams-yaml-add-form select, .reams-root-add-type-form select { min-width: 0; min-height: 1.75rem; box-sizing: border-box; padding: 0 var(--reams-frontmatter-cell-padding); border: 1px solid transparent; border-radius: 2px; background: transparent; color: var(--text-primary); font: inherit; font-size: 0.875em; } .reams-root-type-chips select:hover, .reams-yaml-field input:hover, .reams-yaml-field select:hover, .reams-yaml-field textarea:hover, .reams-yaml-add-form input:hover, .reams-yaml-add-form select:hover, .reams-root-add-type-form select:hover { border-color: var(--text-tertiary); } .reams-root-type-chips select:focus-visible, .reams-root-type-chips button:focus-visible, .reams-yaml-field input:focus-visible, .reams-yaml-field select:focus-visible, .reams-yaml-field textarea:focus-visible, .reams-yaml-field button:focus-visible, .reams-yaml-add-form input:focus-visible, .reams-yaml-add-form select:focus-visible, .reams-yaml-add-form button:focus-visible, .reams-yaml-add:focus-visible, .reams-root-add:focus-visible, .reams-root-add-type-form select:focus-visible, .reams-root-add-type-form button:focus-visible { outline: none; border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: var(--bg-primary, transparent); } .reams-yaml-fields, .reams-yaml-map, .reams-yaml-sequence, .reams-yaml-tags { display: grid; gap: 0.25rem; min-width: 0; } .reams-yaml-map, .reams-yaml-sequence { width: 100%; padding-left: 0.7rem; border-left: 1px solid var(--border-color); box-sizing: border-box; } .reams-yaml-field { display: grid; grid-template-columns: 8rem minmax(0, 1fr) auto; align-items: start; gap: 0.2rem; min-width: 0; overflow: hidden; } .reams-yaml-key { width: 100%; color: var(--text-secondary) !important; } .reams-yaml-value, .reams-yaml-value > input, .reams-yaml-value > textarea, .reams-yaml-value > select { min-width: 0; width: 100%; } .reams-yaml-value textarea { min-height: 4rem; resize: vertical; } .reams-yaml-sequence-item { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 0.2rem; min-width: 0; } .reams-yaml-index { padding-top: 0.3rem; color: var(--text-tertiary); font-size: 0.75rem; } .reams-yaml-add-form { display: grid; grid-template-columns: 8rem minmax(5rem, 0.5fr) minmax(6rem, 1fr) auto auto; gap: 0.2rem; min-width: 0; } .reams-yaml-tags { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.25rem; } .cm-root-frontmatter-surface .reams-yaml-tags button { min-width: 1.75rem; min-height: 1.75rem; } .reams-root-additions { display: flex; flex-wrap: wrap; align-items: start; gap: 0.2rem 0.75rem; min-width: 0; padding-top: 0.15rem; } .reams-root-additions > .reams-yaml-add, .reams-root-additions > .reams-root-add { width: auto; padding-inline: var(--reams-frontmatter-cell-padding); color: var(--text-tertiary); } .reams-root-add-type-form { display: flex; align-items: center; gap: 0.2rem; min-width: 0; } .cm-root-frontmatter-surface .reams-record-section { min-width: 0; } .cm-root-frontmatter-surface .reams-record-fields { padding-top: 0.1rem; } .cm-root-frontmatter-surface .reams-record-fields label { grid-template-columns: 8rem minmax(0, 1fr); gap: 0.2rem; min-height: 1.75rem; padding: 0; overflow: hidden; } .cm-root-frontmatter-surface .reams-record-fields label > span { padding: 0 var(--reams-frontmatter-cell-padding); font-size: 0.875em; } .cm-root-frontmatter-surface .reams-record-fields input:not([type="checkbox"]), .cm-root-frontmatter-surface .reams-record-fields select, .cm-root-frontmatter-surface .reams-record-fields textarea, .cm-root-frontmatter-surface .reams-record-fields fieldset { padding: 0 var(--reams-frontmatter-cell-padding); border-color: transparent; border-radius: 2px; background: transparent; } .cm-root-frontmatter-surface .reams-record-fields input:not([type="checkbox"]):hover, .cm-root-frontmatter-surface .reams-record-fields select:hover, .cm-root-frontmatter-surface .reams-record-fields textarea:hover, .cm-root-frontmatter-surface .reams-record-fields fieldset:hover { border-color: var(--text-tertiary); } .cm-root-frontmatter-surface .reams-record-fields input:not([type="checkbox"]):focus, .cm-root-frontmatter-surface .reams-record-fields select:focus, .cm-root-frontmatter-surface .reams-record-fields textarea:focus { outline: none; border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: var(--bg-primary, transparent); } .cm-root-frontmatter-surface .reams-record-type-chip { min-height: 1.2rem; padding: 0 0.35rem; } @media (max-width: 640px) { .cm-root-frontmatter-surface { width: 100%; margin-inline: 0; } .reams-yaml-field { grid-template-columns: minmax(0, 1fr) auto; gap: 0.25rem; padding-block: 0.2rem; } .reams-yaml-key { grid-column: 1; min-height: 2.5rem !important; padding-inline: var(--reams-frontmatter-cell-padding); } .reams-yaml-value { grid-column: 1 / -1; grid-row: 2; } .reams-yaml-field > .reams-yaml-remove { grid-column: 2; grid-row: 1; } .reams-root-type-chips button, .reams-root-type-chips select, .reams-yaml-field input, .reams-yaml-field select, .reams-yaml-field textarea, .reams-yaml-field button, .reams-yaml-sequence button, .reams-yaml-add, .reams-yaml-add-form input, .reams-yaml-add-form select, .reams-yaml-add-form button, .reams-root-add, .reams-root-add-type-form select, .reams-root-add-type-form button { min-width: 2.5rem; min-height: 2.5rem; } .reams-yaml-add-form { grid-template-columns: minmax(0, 1fr) minmax(6rem, 0.7fr); } .reams-root-additions, .reams-root-add-type-form { width: 100%; } .cm-root-frontmatter-surface .reams-yaml-tags button { min-width: 2.5rem; min-height: 2.5rem; } .reams-root-types { grid-template-columns: minmax(0, 1fr); gap: 0.1rem; } .cm-root-frontmatter-surface .reams-record-fields label { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; min-height: 0; padding-block: 0.2rem; } .cm-root-frontmatter-surface .reams-record-fields label > span { padding-inline: var(--reams-frontmatter-cell-padding); } .cm-root-frontmatter-surface .reams-record-fields input:not([type="checkbox"]), .cm-root-frontmatter-surface .reams-record-fields select, .cm-root-frontmatter-surface .reams-record-fields textarea, .cm-root-frontmatter-surface .reams-record-fields fieldset { min-height: 2.5rem; } .cm-root-frontmatter-surface .reams-root-type-chips .reams-record-type-chip button { min-width: 2.5rem; min-height: 2.5rem; } } /* Root frontmatter deliberately reuses the legacy properties-table DOM and * visual vocabulary. Only Record disclosure and recursive containers are new. */ .cm-root-frontmatter-surface { display: block; width: 100%; max-width: 100%; margin: 0; padding: 0; overflow: visible; } .cm-root-frontmatter-surface .cm-frontmatter-properties-table { margin-bottom: 1em; max-width: 100%; overflow: hidden; } .cm-root-frontmatter-surface .reams-root-types { display: flex; align-items: flex-start; gap: 0.2em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border-color); } .reams-root-types + .reams-root-records, .reams-root-record-group + .reams-root-record-group { margin-top: 0.45em; } .reams-root-records { display: grid; gap: 0.35em; } .reams-root-record-group + .reams-root-record-group { margin-top: 0; } .reams-root-record-group { padding: 0.25em 0.35em 0.35em 0; background: color-mix(in srgb, var(--bg-secondary) 72%, transparent); } .reams-yaml-structure-label { padding: 0 2px 3px; color: var(--text-tertiary); font-size: 0.72em; font-weight: 600; line-height: 1.4; } .reams-root-record-group .cm-frontmatter-prop-value > input:not([type="checkbox"]), .reams-root-record-group .cm-frontmatter-prop-value > select, .reams-root-record-group .cm-frontmatter-prop-value > textarea, .reams-root-record-group .cm-frontmatter-prop-value > .reams-input-suggest > input { background: var(--bg-primary); } .reams-root-record-group .reams-yaml-map, .reams-root-record-group .reams-yaml-sequence, .reams-root-record-group .reams-yaml-json-scalar { container-type: inline-size; gap: 0; margin-block: 0.2em; padding: 5px; border-left: 0; background: var(--bg-primary); } .reams-root-record-group .reams-yaml-map, .reams-root-record-group .reams-yaml-sequence { width: calc(100% - var(--reams-frontmatter-value-inset)); margin-inline-start: var(--reams-frontmatter-value-inset); } .cm-root-frontmatter-surface .cm-frontmatter-prop-row[data-record-field-kind="json"] > .cm-frontmatter-prop-key-readonly { padding-top: calc(0.2em + 5px); } .reams-record-url-link { display: inline-flex; align-items: center; min-width: 0; max-width: 100%; box-sizing: border-box; padding-inline-start: var(--reams-frontmatter-value-inset); color: var(--accent); text-decoration: underline; cursor: pointer; } .reams-record-url-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .reams-root-record-group .reams-yaml-value > .reams-record-url-link { width: 100%; height: var(--reams-frontmatter-control-height); padding-inline: var(--reams-frontmatter-cell-padding); border: 1px solid transparent; border-radius: 2px; background: var(--bg-secondary); font-size: 0.875em; } .reams-root-record-group .reams-yaml-json-scalar { display: grid; } .reams-root-record-group .reams-yaml-json-scalar > input { width: 100%; min-width: 0; box-sizing: border-box; padding: 0 var(--reams-frontmatter-cell-padding); border: 1px solid transparent; border-radius: 2px; color: var(--text-primary); background: var(--bg-secondary); font: inherit; } .reams-root-record-group .reams-yaml-sequence-item > input, .reams-root-record-group .reams-yaml-sequence-item > select, .reams-root-record-group .reams-yaml-sequence-item > textarea { width: 100%; min-width: 0; box-sizing: border-box; padding: 0 var(--reams-frontmatter-cell-padding); border: 1px solid transparent; border-radius: 2px; color: var(--text-primary); background: var(--bg-secondary); font: inherit; } .reams-root-record-group .reams-yaml-field input, .reams-root-record-group .reams-yaml-field select, .reams-root-record-group .reams-yaml-field textarea { background: var(--bg-secondary); } .reams-root-record-group .reams-yaml-field > .reams-yaml-key, .reams-root-record-group .reams-yaml-field > .reams-yaml-value > input, .reams-root-record-group .reams-yaml-field > .reams-yaml-value > select { height: var(--reams-frontmatter-control-height); min-height: var(--reams-frontmatter-control-height); } .reams-root-record-group .reams-yaml-field > .reams-yaml-remove { width: 1.5rem; min-width: 1.5rem; padding-inline: 0; } .reams-root-record-group .reams-yaml-sequence-item > input:hover, .reams-root-record-group .reams-yaml-sequence-item > select:hover, .reams-root-record-group .reams-yaml-sequence-item > textarea:hover { border-color: var(--text-tertiary); } .reams-root-record-group .reams-yaml-sequence-item > input:focus-visible, .reams-root-record-group .reams-yaml-sequence-item > select:focus-visible, .reams-root-record-group .reams-yaml-sequence-item > textarea:focus-visible { outline: none; border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } @media (max-width: 640px) { .cm-root-frontmatter-surface .cm-frontmatter-prop-row[data-record-field-kind="json"] > .cm-frontmatter-prop-key-readonly { padding-top: calc(0.2em + 14px); } .reams-root-record-group .reams-yaml-field { grid-template-columns: minmax(3.5rem, 0.7fr) minmax(0, 1.3fr) 24px; align-items: center; gap: 4px; padding-block: 5px; } .reams-root-record-group .reams-yaml-field > .reams-yaml-key, .reams-root-record-group .reams-yaml-field > .reams-yaml-value, .reams-root-record-group .reams-yaml-field > button { grid-column: auto; grid-row: auto; } .reams-root-record-group .reams-yaml-key, .reams-root-record-group .reams-yaml-value > input, .reams-root-record-group .reams-yaml-value > select, .reams-root-record-group .reams-yaml-sequence-item > input, .reams-root-record-group .reams-yaml-sequence-item > select, .reams-root-record-group .reams-yaml-sequence-item > textarea, .reams-root-record-group .reams-yaml-field > button, .reams-root-record-group .reams-yaml-sequence-item > button, .reams-root-record-group .reams-yaml-sequence > button { height: 34px; min-height: 34px !important; } .reams-root-record-group .reams-yaml-field > .reams-yaml-remove { width: 24px; min-width: 24px; padding-inline: 0; } .reams-root-record-group .reams-yaml-json-scalar > input { height: 34px; min-height: 34px; } .reams-root-record-group .reams-yaml-value > .reams-record-url-link { height: 34px; min-height: 34px; } .reams-root-record-group .reams-yaml-sequence-item { grid-template-columns: 14px minmax(0, 1fr) 34px; align-items: center; gap: 4px; min-height: 44px; padding-block: 5px; } .reams-root-record-group .reams-yaml-index { padding-top: 0; text-align: center; } .reams-root-record-group .reams-yaml-sequence > button { justify-self: end; margin-top: 5px; padding-inline: 8px; } } @container (max-width: 180px) { .reams-root-record-group .reams-yaml-field { grid-template-columns: minmax(0, 1fr) 24px; } .reams-root-record-group .reams-yaml-field > .reams-yaml-key { grid-column: 1; grid-row: 1; } .reams-root-record-group .reams-yaml-field > .reams-yaml-value { grid-column: 1 / -1; grid-row: 2; } .reams-root-record-group .reams-yaml-field > button { grid-column: 2; grid-row: 1; } } .reams-root-types-disclosure { position: relative; min-height: 1.4em; padding: 0; border: 0; background: transparent; cursor: pointer; font: inherit; } .reams-root-types-disclosure:hover, .reams-root-types-disclosure:focus-visible { color: var(--text-primary); } .reams-root-types-disclosure:focus-visible { outline: none; border-radius: 2px; box-shadow: inset 0 0 0 2px var(--accent); } @media (pointer: coarse) { .reams-root-types-disclosure { position: relative; } .reams-root-types-disclosure::after { content: ''; position: absolute; width: 44px; height: 44px; left: 50%; top: 50%; transform: translate(-50%, -50%); } } .reams-yaml-object-summary { padding-inline: var(--reams-frontmatter-value-inset) 0.35em; color: var(--text-tertiary); font-size: 0.9em; } .cm-root-frontmatter-surface .cm-frontmatter-prop-value > .cm-frontmatter-prop-list { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: flex-start; gap: 0.25em; min-width: 0; padding-inline-start: var(--reams-frontmatter-value-inset); } .cm-root-frontmatter-surface .cm-frontmatter-prop-value.cm-frontmatter-prop-list { padding-inline-start: var(--reams-frontmatter-value-inset); } .cm-root-frontmatter-surface .reams-root-types > .cm-frontmatter-prop-value:where(*) { flex-direction: column; flex-wrap: nowrap; gap: 0; box-sizing: border-box; } .cm-root-frontmatter-surface .reams-root-type-chip { align-self: flex-start; } .cm-root-frontmatter-surface .cm-frontmatter-prop-row[data-type="checkbox"] .cm-frontmatter-prop-checkbox { margin-inline-start: var(--reams-frontmatter-value-inset); } .cm-root-frontmatter-surface .reams-record-derived-value { padding-inline-start: var(--reams-frontmatter-value-inset); } .cm-root-frontmatter-surface .cm-frontmatter-prop-value > input:not([type="checkbox"]), .cm-root-frontmatter-surface .cm-frontmatter-prop-value > select, .cm-root-frontmatter-surface .cm-frontmatter-prop-value > textarea { flex: 1 1 12em; width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box; padding: 0 0.35em; border: 1px solid transparent; border-radius: 2px; background: transparent; color: var(--text-primary); font: inherit; } .cm-root-frontmatter-surface .cm-frontmatter-prop-value > input:not([type="checkbox"]):hover, .cm-root-frontmatter-surface .cm-frontmatter-prop-value > select:hover, .cm-root-frontmatter-surface .cm-frontmatter-prop-value > textarea:hover { border-color: var(--text-tertiary); } .cm-root-frontmatter-surface .cm-frontmatter-prop-value > input:not([type="checkbox"]):focus, .cm-root-frontmatter-surface .cm-frontmatter-prop-value > select:focus, .cm-root-frontmatter-surface .cm-frontmatter-prop-value > textarea:focus { outline: none; border-color: var(--accent); background: var(--bg-primary, transparent); } .reams-root-additions { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5em; } .reams-root-legacy-add-form { display: grid; grid-template-columns: 8em minmax(7rem, 0.7fr) minmax(9rem, 1.2fr) auto auto; align-items: center; flex: 1 1 30em; min-width: 0; gap: 0.25em; } .reams-root-legacy-add-form .cm-frontmatter-prop-add-name, .reams-root-legacy-add-form .cm-frontmatter-prop-add-value { width: 100%; min-width: 0; box-sizing: border-box; flex-basis: auto; } .reams-root-add-type-form { grid-template-columns: minmax(8rem, 1fr) auto auto; flex-basis: 18em; } .reams-root-add-structure { min-width: 0; padding: 0 0.35em; color: var(--text-tertiary); font-size: 0.9em; } .reams-frontmatter-icon-editor { display: inline-flex; align-items: center; min-width: 0; gap: 0.3em; } .reams-frontmatter-date-editor { display: inline-grid; grid-template-columns: minmax(0, 1fr) 1.75em; flex: 1 1 12em; align-items: center; min-width: 0; } .reams-frontmatter-date-editor > .cm-frontmatter-prop-input, .reams-frontmatter-date-editor > .cm-frontmatter-prop-add-value { flex: 1 1 auto; width: auto; min-width: 0; } .reams-frontmatter-date-picker-button { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 1.75em; width: 1.75em; height: 1.75em; border: 1px solid transparent; border-radius: 2px; color: var(--text-tertiary); font-size: 0.85em; overflow: hidden; cursor: pointer; } .reams-frontmatter-date-picker-button:hover { color: var(--text-primary); background: var(--bg-hover); } .reams-frontmatter-date-picker { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; cursor: pointer; } .reams-frontmatter-date-picker-button:has(.reams-frontmatter-date-picker:focus-visible) { box-shadow: inset 0 0 0 1px var(--accent); } .reams-frontmatter-icon-editor .cm-frontmatter-prop-input, .reams-frontmatter-icon-editor .cm-frontmatter-prop-add-value { flex: 1 1 auto; min-width: 0; } .reams-frontmatter-icon-preview { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 1.25em; width: 1.25em; height: 1.25em; color: var(--text-secondary); } .reams-frontmatter-icon-preview svg { width: 1em; height: 1em; } @media (max-width: 640px) { .reams-root-legacy-add-form { grid-template-columns: 4em minmax(7rem, 1fr); flex-basis: 100%; } .reams-root-legacy-add-form .reams-root-add-value, .reams-root-legacy-add-form .reams-root-add-structure { grid-column: 1 / -1; } .reams-root-add-type-form { grid-template-columns: minmax(0, 1fr) auto auto; } .cm-root-frontmatter-surface .cm-frontmatter-prop-add-btn, .cm-root-frontmatter-surface .cm-frontmatter-prop-add-cancel { min-height: 2.5rem; } } /* The root metadata surface inherits the established frontmatter table as its * visual contract. These rules cover only the new Type groups and add flows. */ .cm-root-frontmatter-surface .reams-root-additions { display: flex; flex-wrap: wrap; align-items: start; gap: 0.1em 0.75em; width: 100%; } .cm-root-frontmatter-surface .reams-root-additions > .cm-frontmatter-prop-add-btn, .cm-root-frontmatter-surface .reams-root-additions > .reams-root-legacy-add-form { flex: 0 0 auto; } .cm-root-frontmatter-surface .reams-root-additions > .reams-root-legacy-add-form:not(.reams-root-add-type-form) { flex-basis: 100%; } .cm-root-frontmatter-surface .reams-root-additions > .reams-root-add-type-form { flex-basis: 100%; } .cm-root-frontmatter-surface .reams-root-legacy-add-form { display: flex; align-items: flex-start; gap: 0.2em; width: 100%; min-width: 0; } .cm-root-frontmatter-surface .reams-root-add-kind { flex: 0 0 6.75em; width: 6.75em; min-width: 0; appearance: auto; } .cm-root-frontmatter-surface .reams-root-legacy-add-form .reams-root-add-name { flex: 0 0 4em; width: 4em; min-width: 0; } .cm-root-frontmatter-surface .reams-root-add-value { flex: 1 1 8em; width: auto; min-width: 0; } .cm-root-frontmatter-surface .reams-root-add-value > .cm-frontmatter-prop-list { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.25em; min-width: 0; } .cm-root-frontmatter-surface .cm-frontmatter-prop-key-readonly { overflow: hidden; overflow-wrap: normal; text-overflow: ellipsis; white-space: nowrap; } .cm-root-frontmatter-surface .reams-root-add-checkbox { display: inline-flex; align-items: center; min-height: var(--reams-frontmatter-control-height); padding-inline: 0.35em; } .cm-root-frontmatter-surface .reams-root-legacy-add-form > div.reams-root-add-value { align-items: center; min-height: var(--reams-frontmatter-control-height); box-sizing: border-box; } .cm-root-frontmatter-surface .reams-root-add-type-form { display: flex; align-items: center; flex: none; width: auto; } .cm-root-frontmatter-surface .reams-root-add-type-form .cm-frontmatter-prop-add-value { flex: 0 1 12em; width: auto; appearance: auto; font-size: 1em; } .cm-root-frontmatter-surface .reams-root-add-type-form .cm-frontmatter-prop-add-btn:hover, .cm-root-frontmatter-surface .reams-root-add-type-form .cm-frontmatter-prop-add-btn:focus { background: transparent; color: var(--accent); } .cm-root-frontmatter-surface .reams-root-add-type-form .cm-frontmatter-prop-add-cancel:hover { background: transparent; color: var(--text-primary); } .cm-root-frontmatter-surface .reams-frontmatter-date-editor { display: inline-grid; grid-template-columns: minmax(0, 1fr) 1.75em; flex: 1 1 8em; align-items: center; min-width: 0; } .cm-root-frontmatter-surface .reams-frontmatter-date-error { grid-column: 1 / -1; color: var(--error); font-size: 0.75em; line-height: 1.2; } .reams-input-suggest { position: relative; display: inline-flex; flex: 1 1 auto; min-width: 0; } .reams-input-suggest > input { width: 100%; min-width: 0; } .cm-root-frontmatter-surface .cm-frontmatter-prop-value .reams-input-suggest > input { box-sizing: border-box; padding: 0 0.35em; border: 1px solid transparent; border-radius: 2px; outline: none; background: transparent; color: var(--text-primary); font: inherit; } .cm-root-frontmatter-surface .cm-frontmatter-prop-value .reams-input-suggest > input:hover { border-color: var(--text-tertiary); } .cm-root-frontmatter-surface .cm-frontmatter-prop-value .reams-input-suggest > input:focus { border-color: var(--accent); background: var(--bg-primary, transparent); } .reams-input-suggest-menu { position: fixed; box-sizing: border-box; z-index: 1200; display: grid; max-height: 14rem; padding: 0.2rem; border: 1px solid var(--border-color); border-radius: 0.25rem; background: var(--bg-secondary); box-shadow: 0 6px 18px rgb(0 0 0 / 28%); overflow-y: auto; } .reams-input-suggest-menu > button { min-height: 1.75rem; padding: 0.25rem 0.45rem; border: 0; border-radius: 0.2rem; background: transparent; color: var(--text-primary); font: inherit; text-align: left; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .reams-input-suggest-menu > button:hover, .reams-input-suggest-menu > button.is-selected { background: var(--bg-hover); } .reams-reference-list { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: 0.25em; min-width: 0; } .reams-reference-editor { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 0.25rem; } .reams-reference-editor > .reams-input-suggest { flex: 1 1 8rem; min-width: 7rem; } .reams-reference-editor.is-empty { width: 100%; } .reams-reference-editor.is-empty > .reams-reference-add { flex: 1 1 auto; width: 100%; } .reams-reference-chip { display: inline-flex; align-items: center; gap: 0.15em; min-height: 1.5rem; padding-left: 0.35em; border-radius: 0.25rem; background: var(--bg-secondary); color: var(--text-secondary); } .reams-reference-chip-with-wikilink { padding-left: 0; background: transparent; } .reams-reference-chip > button { width: 1.5rem; min-width: 1.5rem; min-height: 1.5rem; margin-inline: -0.25rem -0.35rem; padding: 0; border: 0; border-radius: inherit; background: transparent; color: var(--text-tertiary); font: inherit; line-height: 1; cursor: pointer; } .reams-reference-add { min-height: 1.5rem; padding: 0 0.4rem; border: 1px solid color-mix(in srgb, var(--text-secondary) 35%, transparent); border-radius: 2px; background: transparent; color: var(--text-secondary); font: inherit; font-size: 0.875em; line-height: 1; cursor: pointer; } .reams-reference-add:hover { border-color: var(--text-secondary); background: var(--bg-hover); color: var(--text-primary); } .reams-reference-add:focus-visible, .reams-reference-chip > button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } .cm-root-frontmatter-surface .reams-frontmatter-date-editor > input[aria-invalid="true"] { border-color: var(--error); } .cm-root-frontmatter-surface .reams-frontmatter-date-editor > input[type="text"] { flex: 1 1 auto; width: auto; min-width: 0; } .reams-frontmatter-date-picker-icon, .reams-frontmatter-date-picker-icon svg { display: block; width: 0.8em; height: 0.8em; } @media (min-width: 768px) { .cm-root-frontmatter-surface .reams-root-legacy-add-form .reams-root-add-name { flex-basis: 8em; width: 8em; } } @media (max-width: 520px) { .cm-root-frontmatter-surface .reams-root-legacy-add-form:not(.reams-root-add-type-form) { display: grid; grid-template-columns: 6.75em minmax(0, 1fr) auto; align-items: center; } .cm-root-frontmatter-surface .reams-root-legacy-add-form:not(.reams-root-add-type-form) .reams-root-add-kind { grid-column: 1; grid-row: 1; } .cm-root-frontmatter-surface .reams-root-legacy-add-form:not(.reams-root-add-type-form) .reams-root-add-name { grid-column: 2; grid-row: 1; width: 100%; } .cm-root-frontmatter-surface .reams-root-legacy-add-form:not(.reams-root-add-type-form) .reams-root-add-value { grid-column: 1 / 3; grid-row: 2; width: 100%; } .cm-root-frontmatter-surface .reams-root-legacy-add-form:not(.reams-root-add-type-form) > .cm-frontmatter-prop-add-btn { grid-column: 3; grid-row: 2; } .cm-root-frontmatter-surface .reams-root-legacy-add-form:not(.reams-root-add-type-form) > .cm-frontmatter-prop-add-cancel { grid-column: 3; grid-row: 1; } } .reams-record-type-chip { min-height: 1.2rem; font-weight: 650; letter-spacing: 0.02em; } .reams-record-date-editor { position: relative; display: inline-flex; align-items: center; min-width: 0; } .reams-record-date-editor > input { width: 100%; min-width: 0; } .reams-record-fields { display: grid; gap: 0.05rem; padding-top: 0.25rem; } .reams-record-fields label { display: grid; grid-template-columns: minmax(4.5rem, 0.55fr) minmax(7rem, 1.45fr); align-items: center; gap: 0.4rem; min-width: 0; min-height: 1.7rem; padding: 0.1rem 0; } .reams-record-fields label > span { color: var(--text-secondary); font-size: 0.875em; } .reams-record-fields input:not([type="checkbox"]), .reams-record-fields select, .reams-record-fields textarea { min-width: 0; width: 100%; box-sizing: border-box; min-height: 1.7rem; padding: 0.15rem 0.4rem; border: 1px solid color-mix(in srgb, var(--text-secondary) 45%, transparent); border-radius: 2px; background: var(--background-primary); color: var(--text-primary); font: inherit; font-size: 0.875em; } .reams-record-fields textarea { min-height: 3.4rem; resize: vertical; } .reams-record-fields input:not([type="checkbox"]):hover, .reams-record-fields select:hover, .reams-record-fields textarea:hover { border-color: var(--text-secondary); } .reams-record-fields input:not([type="checkbox"]):focus, .reams-record-fields select:focus, .reams-record-fields textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); } .reams-record-fields input[type="checkbox"] { width: 1rem; height: 1rem; margin: 0; accent-color: var(--accent); cursor: pointer; } .reams-record-fields fieldset { min-width: 0; margin: 0; padding: 0.25rem 0.4rem; border: 1px solid color-mix(in srgb, var(--text-secondary) 35%, transparent); border-radius: 2px; } .reams-record-derived-value { min-width: 0; overflow: hidden; color: var(--text-secondary); font-size: 0.875em; text-overflow: ellipsis; white-space: nowrap; } .cm-record-node-glyph { display: inline-grid; place-items: center; width: 1.35rem; height: 1.5rem; padding: 0; border: 0; border-radius: 2px; background: transparent; color: var(--text-tertiary); cursor: pointer; font: inherit; line-height: 1; } .cm-record-node-glyph svg, .cm-list-bullet-glyph-record svg { width: 1em; height: 1em; display: block; } .cm-record-node-glyph:hover, .cm-list-bullet-glyph-record:hover { color: var(--accent); background: var(--bg-hover); } .cm-record-node-glyph[data-active="true"], .cm-list-bullet-glyph-record[data-active="true"] { color: var(--accent); } .cm-record-node-glyph[aria-expanded="false"]:not([data-active="true"]), .cm-list-bullet-glyph-record[aria-expanded="false"]:not([data-active="true"]) { opacity: 0.58; } .cm-record-node-glyph:focus-visible, .cm-list-bullet-glyph-record:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } @media (pointer: coarse) { .cm-record-node-glyph { width: 2.25rem; height: 2.25rem; margin-inline: -0.35rem; } .reams-record-fields label { min-height: 2.25rem; } .reams-record-fields input:not([type="checkbox"]), .reams-record-fields select { height: 1.7rem; min-height: 1.7rem; } } @media (max-width: 640px) { .cm-block-record-sections { width: calc(100% + 1rem); max-width: none; margin: 2px -0.5rem; padding: 2px 0.75rem 0.3rem; border-radius: 2px; } .reams-record-type-chip { min-height: 1.1rem; padding-inline: 0.25rem; border-radius: 2px; } .reams-record-fields { gap: 0; padding-top: 0.1rem; } .reams-record-fields label { grid-template-columns: minmax(4.25rem, 0.5fr) minmax(0, 1.5fr); gap: 0.25rem; padding-block: 0; } } /* CM6 makes `.cm-scroller` a flex container. The content root is therefore a * flex item, and its default flex-item `min-width: auto` lets wide frontmatter * widgets pin the item's min-content width to 42rem on phones. That expands the * editor scroller horizontally and steals swipe gestures from the app shell. * Force the content root to shrink inside the scroller so frontmatter can * reflow vertically without widening the editor. */ .cm6-editor-container .cm-scroller > .cm-content { min-width: 0 !important; } /* ---- Heading styles (decoration layer — live-preview line classes) ---- */ .cm-heading { font-weight: 700; } .cm-heading-1 { font-size: 2em; line-height: 1.2; } .cm-heading-2 { font-size: 1.5em; line-height: 1.25; } .cm-heading-3 { font-size: 1.17em; line-height: 1.3; } .cm-heading-4 { font-size: 1em; line-height: 1.4; } .cm-heading-5 { font-size: 0.83em; line-height: 1.45; } .cm-heading-6 { font-size: 0.67em; line-height: 1.5; } /* ---- Hidden formatting markers ---- */ .cm-formatting-hidden { /* HYBRID IDIOM 2 — see hidden-text-classes.ts (#269). * * Used by heading-decoration.ts to hide the `# ` / `## ` / etc. * prefix on non-cursor lines. Pre-fix `display: none` removed the * prefix from layout entirely; for an empty heading whose only * visible content is `# `, the line had no caret-targetable surface * and ArrowUp/ArrowDown skipped it. * * Post-fix HYBRID idiom: zero VISIBLE width (so heading text * still starts at column 0) but NON-ZERO height (so an empty * heading line has a caret-targetable inline-block). * * `display: inline-block; width: 0; max-width: 0; overflow: hidden` * zeroes visible inline width — heading text after the prefix * starts at column 0, matching pre-fix layout. * * `height: 1em; line-height: 1em` * keeps non-zero vertical extent, so an empty `# ` line has * caret-targetable geometry. This is what makes it different * from the rolled-back idiom that re-introduces the cursor- * skip bug. * * `color: transparent; user-select: none` * defensive — paint nothing if `overflow: hidden` doesn't * fully clip in some browser quirk; prevent drag-selection. * * The heading line decoration (`.cm-heading-N` font-size + * line-height) provides the dominant visual treatment * regardless of prefix visibility. */ display: inline-block; width: 0; max-width: 0; height: 1em; line-height: 1em; overflow: hidden; color: transparent; user-select: none; } /* Blockquote/callout prefixes need a less aggressive collapse than * `display: none`. Using zero-width inline-block keeps the DOM positions * participating in caret motion, which avoids ArrowUp/ArrowDown jumping out * of long quoted blocks in some browsers. * * `height: 0; line-height: 0; vertical-align: top` mirrors the same fix * applied to `.cm-inline-formatting-hidden` below: without these, the * inline-block inherits the parent line-height and grows the surrounding * line box on top of the natural inline content's ascent/descent. * Visible callout chrome (background, padding) usually masks the resulting * line jump, but the underlying CSS pathology is identical to the * inline-formatting-hidden case — apply prophylactically so a future * theme change doesn't surface it. */ .cm-blockquote-formatting-hidden { display: inline-block; width: 0; max-width: 0; height: 0; line-height: 0; vertical-align: top; overflow: hidden; white-space: pre; opacity: 0; } .cm-inline-formatting-hidden { /* CLICK-SNAP FIX (#269 add-on): `display: none` removes the marker * span from layout AND from caret hit-testing entirely — clicks at * the visible left edge of `**bold**` resolve to position 'b' * (start of the visible content), not to the source position of the * hidden `**` marker. The earlier (pre-fix) zero-geometry inline- * block kept the marker in the DOM as a zero-rect hit target; * caretRangeFromPoint snapped to it, producing the user-reported * "cursor lands at marker boundary" bug. * * Why `display: none` is safe here even though #269 used the * zero-geometry idiom for the same-shape problem on whole-line * collapses: this class is only ever applied to INLINE markers * (bold/italic/strike/`** `/`*`/`~~`/backticks). The line ALWAYS * has visible text on at least one side of the marker (the markup * always wraps content). The whole-line cursor-skip mechanism that * #269 originally addressed required a `.cm-line` whose ENTIRE * visible content was zero-collapsed — that does not occur for * inline markers, so `display: none` here cannot regress the * cursor-skip class. * * Belt-and-braces: the inline-format-decoration plugin and * code-marker-decoration plugin also register their hidden-marker * RangeSets via `EditorView.atomicRanges` (see those files' * doc-comments and node_modules/@codemirror/view/dist/index.d.ts: * 1311-1315 — the documented way to make collapsing decorations * behave atomically for cursor motion). atomicRanges' boundary * caveat (`pos > from && pos < to`, see index.js:3708) means * positions exactly at `from`/`to` are still passed through; the * `display: none` rendering ensures caretRangeFromPoint never * resolves to those boundary positions in the first place. * * Affects: bold/italic/strike marker collapse (inline-format-decoration.ts) * and inline/fenced code marker collapse (code-marker-decoration.ts). */ display: none; } /* ---- Inline formatting styles (decoration layer) ---- */ .cm-bold { font-weight: 700; } .cm-italic { font-style: italic; } .cm-strikethrough { text-decoration: line-through; } /* Wikilink chip styling lives in src/components/wikilink-chip.css so editor * widgets and sidebar link panels share one canonical visual. */ /* ---- Markdown syntax classes: LAYOUT (all modes) * * These rules apply in every mode. They cover weight/style/font-family — * the bits that are part of the structural visual, not coloring. Colors * live under `[data-mode="source"]` below so live-preview mode stays * monochrome prose with only weight/italic/etc cues. * * TWO-LAYER ARCHITECTURE (Strategy Gate decision #8 in * docs/plans/2026-04-22-syntax-highlighting-visual-theming.md): * - HighlightStyle classes (cm-md-*) are emitted by the language parser * via syntaxHighlighting(). They cover source mode and are the only * token classes present when live-preview is disabled. * - Decoration ViewPlugin classes (cm-heading, cm-bold, cm-italic, * cm-strikethrough, plus .cm-heading-1..6 line classes) are applied * by extensions in src/editor/extensions/*. They cover live-preview * mode and also drive the per-heading font-size stack. * The two layers deliberately overlap on font-weight / font-style so * that either layer alone produces the correct visual. Font-size lives * only in the decoration layer to avoid compounding. * * SCOPE NOTE for `.cm-md-list` and `.cm-md-quote`: * @lezer/markdown applies `tags.list` / `tags.quote` to the ENTIRE * list-item / quote-line content (marker + body), not only to the * marker. Applying a color rule to `.cm-md-list` would tint every list * item's body text. We therefore leave `.cm-md-list` with no color. * Blockquote `.cm-md-quote` keeps `font-style: italic` (a prose * convention expected for the whole line). * -------------------------------------------------------------------------- */ .cm-md-strong { font-weight: 700; } .cm-md-em { font-style: italic; } .cm-md-strike { text-decoration: line-through; } .cm-md-quote { font-style: italic; } .cm-md-code { font-family: var(--font-mono); } /* Inline `code` chrome — sunken background, small padding, rounded. * * Scoped via :not(.cm-codeblock *) so the inline-code visual chrome only * applies to backtick spans in prose, NOT to the cm-md-code spans that * appear inside a fenced/indented code block (those already get the * `.cm-codeblock` slab via the line decoration). The line-class selector * `.cm-line.cm-codeblock .cm-md-code` would suppress this rule with the * higher-specificity `:not()` form below. * * Visible in all modes: source, live-preview, and read. The inline-code * convention is universal and the chrome is muted enough to not dominate. */ .cm-line:not(.cm-codeblock) .cm-md-code { background-color: var(--code-bg-sunken); padding: 0.05em 0.35em; border-radius: 0.25em; /* Hide the surrounding backticks visually only in non-source modes; * source mode still wants them for editing. The .cm-md-punctuation * tertiary tint already provides enough contrast in source. */ } .cm-md-comment { font-style: italic; } /* ---- Markdown syntax classes: COLORS (source mode only) * * Per user decision: colors apply only when the editor is showing raw * markdown (source mode). Live-preview mode renders prose-like text with * font weight/style only. Read mode hides syntax markers entirely (see * `[data-mode="read"]` block farther down). * * HEADING SPECIFICS: * The warm-orange heading tint applies only in source mode. Font-weight * is intentionally NOT set on `.cm-md-heading` — the user wants color * alone to distinguish heading text in source mode. In live-preview * mode the `.cm-heading` line decoration still applies `font-weight: 700` * via the decoration layer. * * LIST MARKER EXCEPTION: * Every list item's marker (`-`, `*`, `1.`) is tagged by @lezer/markdown * as BOTH `list` and `processingInstruction`, so the marker span carries * `cm-md-list cm-md-punctuation`. We exempt list markers from the * punctuation dim tint by overriding to `inherit` when both classes are * present — keeps markers readable in the default text color. * -------------------------------------------------------------------------- */ .cm6-editor-container[data-mode="source"] .cm-md-heading { color: var(--syntax-heading); } .cm6-editor-container[data-mode="source"] .cm-md-code { color: var(--syntax-code); } .cm6-editor-container[data-mode="source"] .cm-md-link { color: var(--accent); } .cm6-editor-container[data-mode="source"] .cm-md-url { color: var(--accent); } .cm6-editor-container[data-mode="source"] .cm-md-hr { color: var(--text-tertiary); } .cm6-editor-container[data-mode="source"] .cm-md-meta { color: var(--syntax-callout); } .cm6-editor-container[data-mode="source"] .cm-md-punctuation { color: var(--text-tertiary); } .cm6-editor-container[data-mode="source"] .cm-md-comment { color: var(--text-tertiary); } .cm6-editor-container[data-mode="source"] .cm-md-wikilink-bracket { color: var(--text-tertiary); } .cm6-editor-container[data-mode="source"] .cm-md-label { color: var(--text-secondary); } .cm6-editor-container[data-mode="source"] .cm-md-escape { color: var(--text-tertiary); } .cm6-editor-container[data-mode="source"] .cm-md-invalid { color: var(--error); } .cm6-editor-container[data-mode="source"] .cm-md-frontmatter { color: var(--syntax-frontmatter); } .cm6-editor-container[data-mode="source"] .cm-md-frontmatter-fence { color: var(--text-tertiary); } /* ---- Markdown link rendering (live-preview + read mode) ---- * * `cm-md-inline-link-label` is the visible label of a `[label](url)` * link when the cursor is far from it (a Decoration.replace widget * collapsing brackets/URL/parens). `cm-md-bare-url` covers * `` autolink URL ranges and top-level GFM bare URL ranges. * * The accent + underline + pointer cursor combine to communicate "this * is a hyperlink" — matching browser default link affordance. */ /* Link labels and external arrows share components/external-link.css with UI links. */ /* Source mode: bare URLs keep their existing accent colour (set on * line 250 by the `.cm-md-url` rule) and gain an underline as a * web-browser-style affordance. NO new colour is added; this rule * intentionally adds ONLY text-decoration. */ .cm6-editor-container[data-mode="source"] .cm-md-url { text-decoration: underline; } /* External-link icon (diagonal arrow): rendered as a separate * `Decoration.widget` (`ExternalLinkIconWidget` in * inline-link-decoration.ts) and emitted at the end of each link / * bare-URL range, mirroring `CalloutIconWidget`'s emission pattern. * * Conditional emission — the widget is NOT added when the link is on * the cursor's line. This is the same chrome-hiding-on-cursor-line * idiom callout/blockquote use. We deliberately do NOT use a CSS * `display: none` toggle here: that would fail the * `cm6-editor-css-hide-audit` test, which exists specifically to * prevent display-none-as-suppression patterns on `.cm-line` * descendants. * * Inline `` with `stroke="currentColor"` so the icon inherits * the surrounding link text colour automatically — no per-theme * overrides needed. Source mode never emits the widget at all (the * inline-link-decoration extension is mounted only via livePreview()). * Wikilink chips (`.cm-wikilink-chip`) are NOT decorated with this * widget; they are internal navigation, not external. */ /* Frontmatter override: the line class paints the whole line with * --syntax-frontmatter, but nested parser spans (cm-md-heading, cm-md-link, * cm-md-punctuation, etc.) would otherwise override that inherited color. * Clamp every nested cm-md-* descendant back to inherit, EXCEPT the fence * marks which intentionally stay tertiary gray. */ .cm6-editor-container[data-mode="source"] .cm-md-frontmatter [class*="cm-md-"]:not(.cm-md-frontmatter-fence) { color: inherit !important; font-weight: inherit !important; font-style: inherit !important; text-decoration: none !important; font-family: inherit !important; } /* List marker exemption: an element with BOTH cm-md-list and cm-md-punctuation * is a list marker (`-`, `*`, `1.`). Keep it in the default text color in * source mode rather than dimmed. Specificity: 0,3,0 (three classes) beats * the `.cm6-editor-container[data-mode="source"] .cm-md-punctuation` rule * above which is 0,2,0 + one attribute = 0,3,0. Tie-break by source order * puts this rule last, so it wins. */ .cm6-editor-container[data-mode="source"] .cm-md-list.cm-md-punctuation { color: inherit; } /* Also exempt link-marker brackets that appear inside wikilinks from the * punctuation tint — the outer-bracket decoration will already paint them * via cm-md-wikilink-bracket. Without this override, the inner `[` and `]` * inside `[[target]]` would remain tertiary-gray while the OUTER brackets * are wikilink-bracket tertiary-gray too — visually identical, so this is * cosmetic. Left in place for documentation. */ /* ---- Callout blocks (live-preview + read mode) ---- * * Callout styling uses CSS-only chrome: border-left for type color, * background tint, and ::before for icons (future). * * NO ch-based offset math. Line decorations apply to .cm-line elements * and CSS handles all layout. * * Source mode note: calloutDecoration is only loaded via livePreview(), * which is disabled in source mode. These styles only apply in * live-preview and read modes. * -------------------------------------------------------------------------- */ /* Each type-specific rule declares the rgba fallback first, then attempts * the `color-mix(in srgb, ...)` derivation. Browsers that don't support * `color-mix()` (Safari <16.2, Firefox <113) drop the second rule and fall * back to the rgba precomputed in `--syntax-callout--bg`. Browsers * that DO support it use the derivation, which keeps the tint in lockstep * with the solid color variable if a future palette tweak only changes the * solid hex. */ /* Blockquote / callout chrome with ancestor flow-through. * * Layout: * - `margin-left: (depth-1) * indent`. Each nesting level visually sits * INSIDE its parent: the inner block's box starts to the right of * where the parent's chrome ends. This is the classic "nested boxes" * reading. * - `padding-left: 1em` for the content gutter inside the line. * - `border-left: bar-width solid ` for the inner block's * own accent. * - The PARENT's chrome (bar + tint at each ancestor depth) is painted * by a `::before` pseudo-element that extends LEFTWARD into the * margin gutter. The pseudo's `background-image` is a stack of * `linear-gradient` layers, one per ancestor depth, each painting a * solid stripe followed by a translucent tint at that depth's slot. * * Why ::before + linear-gradient (not background gradients on the line * itself, and not box-shadow): * - Each nested level should have its OWN clean tint with no mixing. * `background-color` on the inner block paints just the inner box, * no overlap. * - The parent's chrome lives OUTSIDE the inner block, in the margin * area painted by `::before`. The line's own background-color * doesn't reach there, so no blending across boundaries. * - Each ancestor's slot in the gradient stack is non-overlapping * (slot K spans `(K-1)*indent` to `K*indent`), so the layered * gradients render independent strips with no alpha mixing. * - Box-shadows can't paint a tall thin vertical bar OUTSIDE the * element's box without bleeding the spread vertically too; * ::before with absolute positioning is the cleanest primitive. * * The decoration extension (callout-decoration.ts:buildAncestorChainStyle) * emits per-line CSS variables (`--cm-bq-color-1`..`--cm-bq-color-N-1`) * that the static gradient stack reads. */ .cm-callout, .cm-quote { /* Indent step per nesting level. The line is indented by * `(depth - 1) * --cm-bq-indent` via margin-left, so visually each * nesting level sits inside its parent. */ --cm-bq-indent: 1.5rem; --cm-bq-bar-width: 4px; --cm-bq-tint-alpha: 10%; /* Lists reserve a 6px marker rail before their glyph. Starting quote and * callout chrome on that same rail makes mixed list/callout notes scan as a * single aligned column instead of making callouts look inset or floating. */ margin-left: calc(6px + (var(--cm-blockquote-depth, 1) - 1) * var(--cm-bq-indent)); padding-left: 1em; border-left: var(--cm-bq-bar-width) solid var(--cm-bq-self-color, var(--text-tertiary)); /* Establish a positioning context so ::before can extend leftward into * the margin gutter to paint ancestor chrome. */ position: relative; } /* Parent-chrome flow-through: a single ::before pseudo-element on every * blockquote / callout line that EXTENDS LEFTWARD from the line's left * edge into the margin gutter. The pseudo's background is a stack of * `linear-gradient`s — one per ancestor depth — each painting a thin * solid stripe (the parent's bar) followed by a translucent tint to the * right of that bar. * * Width of ::before is the total leftward distance: `(depth - 1) * indent`. * No content paints over this area because the line's own box starts at * `left: 0` of the pseudo's right edge. * * Each ancestor depth K (1..depth-1) paints at horizontal position * `(K - 1) * indent` measured from the EDITOR's left edge, i.e. from * the leftmost edge of the ::before. So the gradient stop math is * `(K-1) * indent` to `(K-1) * indent + bar-width`. * * The line's OWN box (.cm-line) is to the right of the ::before and * paints its own border + bg-color separately, so the inner block has * a clean tint with no color mixing from ancestors. * * Depth ceiling: 12 layered gradients — comfortable headroom for any * realistic Obsidian-flavored markdown. Adversary-review M1 flagged * that the JS emits `--cm-bq-color-K` for any K (no cap), so a stack * shorter than the deepest realistic doc would silently lose the * deepest ancestor's bar. 12 is more than any plausible note nesting; * if a future doc somehow exceeds it, the slot just paints transparent * (graceful degradation). */ .cm-callout::before, .cm-quote::before { content: ""; position: absolute; top: 0; bottom: 0; /* Anchor the right edge of ::before at the OUTER edge of the line's * own border-left (where the margin gutter ENDS). With `position: * relative` on the line, an absolutely-positioned ::before's * containing block is the line's PADDING box — so `right: 100%` * alone would place the right edge at the inner edge of border-left, * shifted INWARD by `--cm-bq-bar-width`. Add that bar-width back so * the parent's bar painted by the gradient lines up with the actual * parent line's border-left position above and below the nested block. * * Without this offset the parent's bar in the margin would sit * ~4px to the right of the parent's bar on its own lines — a * "step" at the boundary of nested blocks. */ right: calc(100% + var(--cm-bq-bar-width)); width: calc((var(--cm-blockquote-depth, 1) - 1) * var(--cm-bq-indent)); pointer-events: none; /* Stacked gradient layers — depths 1..12. Each layer is independent * (no alpha blending) because they don't overlap horizontally; each * ancestor's stripe + tint occupies its own `indent`-wide slot. */ background-image: /* depth 1 */ linear-gradient(to right, transparent 0, transparent calc(0 * var(--cm-bq-indent)), var(--cm-bq-color-1, transparent) calc(0 * var(--cm-bq-indent)), var(--cm-bq-color-1, transparent) calc(0 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-1, transparent) var(--cm-bq-tint-alpha), transparent) calc(0 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-1, transparent) var(--cm-bq-tint-alpha), transparent) calc(1 * var(--cm-bq-indent)), transparent calc(1 * var(--cm-bq-indent)) ), /* depth 2 */ linear-gradient(to right, transparent 0, transparent calc(1 * var(--cm-bq-indent)), var(--cm-bq-color-2, transparent) calc(1 * var(--cm-bq-indent)), var(--cm-bq-color-2, transparent) calc(1 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-2, transparent) var(--cm-bq-tint-alpha), transparent) calc(1 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-2, transparent) var(--cm-bq-tint-alpha), transparent) calc(2 * var(--cm-bq-indent)), transparent calc(2 * var(--cm-bq-indent)) ), /* depth 3 */ linear-gradient(to right, transparent 0, transparent calc(2 * var(--cm-bq-indent)), var(--cm-bq-color-3, transparent) calc(2 * var(--cm-bq-indent)), var(--cm-bq-color-3, transparent) calc(2 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-3, transparent) var(--cm-bq-tint-alpha), transparent) calc(2 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-3, transparent) var(--cm-bq-tint-alpha), transparent) calc(3 * var(--cm-bq-indent)), transparent calc(3 * var(--cm-bq-indent)) ), /* depth 4 */ linear-gradient(to right, transparent 0, transparent calc(3 * var(--cm-bq-indent)), var(--cm-bq-color-4, transparent) calc(3 * var(--cm-bq-indent)), var(--cm-bq-color-4, transparent) calc(3 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-4, transparent) var(--cm-bq-tint-alpha), transparent) calc(3 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-4, transparent) var(--cm-bq-tint-alpha), transparent) calc(4 * var(--cm-bq-indent)), transparent calc(4 * var(--cm-bq-indent)) ), /* depth 5 */ linear-gradient(to right, transparent 0, transparent calc(4 * var(--cm-bq-indent)), var(--cm-bq-color-5, transparent) calc(4 * var(--cm-bq-indent)), var(--cm-bq-color-5, transparent) calc(4 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-5, transparent) var(--cm-bq-tint-alpha), transparent) calc(4 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-5, transparent) var(--cm-bq-tint-alpha), transparent) calc(5 * var(--cm-bq-indent)), transparent calc(5 * var(--cm-bq-indent)) ), /* depth 6 */ linear-gradient(to right, transparent 0, transparent calc(5 * var(--cm-bq-indent)), var(--cm-bq-color-6, transparent) calc(5 * var(--cm-bq-indent)), var(--cm-bq-color-6, transparent) calc(5 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-6, transparent) var(--cm-bq-tint-alpha), transparent) calc(5 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-6, transparent) var(--cm-bq-tint-alpha), transparent) calc(6 * var(--cm-bq-indent)), transparent calc(6 * var(--cm-bq-indent)) ), /* depth 7 */ linear-gradient(to right, transparent 0, transparent calc(6 * var(--cm-bq-indent)), var(--cm-bq-color-7, transparent) calc(6 * var(--cm-bq-indent)), var(--cm-bq-color-7, transparent) calc(6 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-7, transparent) var(--cm-bq-tint-alpha), transparent) calc(6 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-7, transparent) var(--cm-bq-tint-alpha), transparent) calc(7 * var(--cm-bq-indent)), transparent calc(7 * var(--cm-bq-indent)) ), /* depth 8 */ linear-gradient(to right, transparent 0, transparent calc(7 * var(--cm-bq-indent)), var(--cm-bq-color-8, transparent) calc(7 * var(--cm-bq-indent)), var(--cm-bq-color-8, transparent) calc(7 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-8, transparent) var(--cm-bq-tint-alpha), transparent) calc(7 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-8, transparent) var(--cm-bq-tint-alpha), transparent) calc(8 * var(--cm-bq-indent)), transparent calc(8 * var(--cm-bq-indent)) ), /* depth 9 */ linear-gradient(to right, transparent 0, transparent calc(8 * var(--cm-bq-indent)), var(--cm-bq-color-9, transparent) calc(8 * var(--cm-bq-indent)), var(--cm-bq-color-9, transparent) calc(8 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-9, transparent) var(--cm-bq-tint-alpha), transparent) calc(8 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-9, transparent) var(--cm-bq-tint-alpha), transparent) calc(9 * var(--cm-bq-indent)), transparent calc(9 * var(--cm-bq-indent)) ), /* depth 10 */ linear-gradient(to right, transparent 0, transparent calc(9 * var(--cm-bq-indent)), var(--cm-bq-color-10, transparent) calc(9 * var(--cm-bq-indent)), var(--cm-bq-color-10, transparent) calc(9 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-10, transparent) var(--cm-bq-tint-alpha), transparent) calc(9 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-10, transparent) var(--cm-bq-tint-alpha), transparent) calc(10 * var(--cm-bq-indent)), transparent calc(10 * var(--cm-bq-indent)) ), /* depth 11 */ linear-gradient(to right, transparent 0, transparent calc(10 * var(--cm-bq-indent)), var(--cm-bq-color-11, transparent) calc(10 * var(--cm-bq-indent)), var(--cm-bq-color-11, transparent) calc(10 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-11, transparent) var(--cm-bq-tint-alpha), transparent) calc(10 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-11, transparent) var(--cm-bq-tint-alpha), transparent) calc(11 * var(--cm-bq-indent)), transparent calc(11 * var(--cm-bq-indent)) ), /* depth 12 */ linear-gradient(to right, transparent 0, transparent calc(11 * var(--cm-bq-indent)), var(--cm-bq-color-12, transparent) calc(11 * var(--cm-bq-indent)), var(--cm-bq-color-12, transparent) calc(11 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-12, transparent) var(--cm-bq-tint-alpha), transparent) calc(11 * var(--cm-bq-indent) + var(--cm-bq-bar-width)), color-mix(in srgb, var(--cm-bq-color-12, transparent) var(--cm-bq-tint-alpha), transparent) calc(12 * var(--cm-bq-indent)), transparent calc(12 * var(--cm-bq-indent)) ); } .cm-quote { --cm-bq-bar-width: 2px; --cm-bq-self-color: var(--text-tertiary); } .cm-callout { --cm-bq-self-color: var(--syntax-callout-note); background-color: var(--syntax-callout-note-bg); background-color: color-mix(in srgb, var(--syntax-callout-note) 10%, transparent); } .cm-callout-header { font-weight: 600; } /* Type-specific accent color and background tint. * * `--cm-bq-self-color` is consumed by the static `border-left: * var(--cm-bq-bar-width) solid var(--cm-bq-self-color)` rule on * `.cm-callout, .cm-quote` to paint the current depth's vertical bar. * The translucent `background-color` here paints the line's own tint * — the inner block's box. Ancestor bars and tints are painted by the * `::before` pseudo-element using `--cm-bq-color-K` vars emitted from * the decoration extension's `buildAncestorChainStyle`. */ .cm-callout-note { --cm-bq-self-color: var(--syntax-callout-note); background-color: var(--syntax-callout-note-bg); background-color: color-mix(in srgb, var(--syntax-callout-note) 10%, transparent); } .cm-callout-abstract { --cm-bq-self-color: var(--syntax-callout-abstract); background-color: var(--syntax-callout-abstract-bg); background-color: color-mix(in srgb, var(--syntax-callout-abstract) 10%, transparent); } .cm-callout-info { --cm-bq-self-color: var(--syntax-callout-info); background-color: var(--syntax-callout-info-bg); background-color: color-mix(in srgb, var(--syntax-callout-info) 10%, transparent); } .cm-callout-todo { --cm-bq-self-color: var(--syntax-callout-todo); background-color: var(--syntax-callout-todo-bg); background-color: color-mix(in srgb, var(--syntax-callout-todo) 10%, transparent); } .cm-callout-tip { --cm-bq-self-color: var(--syntax-callout-tip); background-color: var(--syntax-callout-tip-bg); background-color: color-mix(in srgb, var(--syntax-callout-tip) 10%, transparent); } .cm-callout-success { --cm-bq-self-color: var(--syntax-callout-success); background-color: var(--syntax-callout-success-bg); background-color: color-mix(in srgb, var(--syntax-callout-success) 10%, transparent); } .cm-callout-question { --cm-bq-self-color: var(--syntax-callout-question); background-color: var(--syntax-callout-question-bg); background-color: color-mix(in srgb, var(--syntax-callout-question) 10%, transparent); } .cm-callout-warning { --cm-bq-self-color: var(--syntax-callout-warning); background-color: var(--syntax-callout-warning-bg); background-color: color-mix(in srgb, var(--syntax-callout-warning) 10%, transparent); } .cm-callout-failure { --cm-bq-self-color: var(--syntax-callout-failure); background-color: var(--syntax-callout-failure-bg); background-color: color-mix(in srgb, var(--syntax-callout-failure) 10%, transparent); } .cm-callout-danger { --cm-bq-self-color: var(--syntax-callout-danger); background-color: var(--syntax-callout-danger-bg); background-color: color-mix(in srgb, var(--syntax-callout-danger) 10%, transparent); } .cm-callout-bug { --cm-bq-self-color: var(--syntax-callout-bug); background-color: var(--syntax-callout-bug-bg); background-color: color-mix(in srgb, var(--syntax-callout-bug) 10%, transparent); } .cm-callout-example { --cm-bq-self-color: var(--syntax-callout-example); background-color: var(--syntax-callout-example-bg); background-color: color-mix(in srgb, var(--syntax-callout-example) 10%, transparent); } .cm-callout-quote { --cm-bq-self-color: var(--syntax-callout-quote); background-color: var(--syntax-callout-quote-bg); background-color: color-mix(in srgb, var(--syntax-callout-quote) 10%, transparent); } /* ---- Callout type icons (#252) ---- * * `cm-callout-icon-wrapper` is the emitted by CalloutIconWidget; * it wraps the Lucide-built . * * Layout intent: the icon sits BEFORE the rendered title (or before the * hidden `> [!type]` chrome on inactive lines), inline-flex with a small * margin-inline-end (NOT flex `gap` — see the wrapper rule below) so it * has consistent breathing room from the title text in both live-preview * and read mode. * * `vertical-align: -0.125em` aligns the 1em-square SVG with the text * baseline (Lucide canvas centers the glyph 0.5em above baseline; dropping * by 0.125em puts the optical center close to the cap height of body text). * * The icon stroke uses currentColor (set on the SVG itself), and the * per-type rules below set `color` on `.cm-callout- .cm-callout-icon` * so the stroke matches each type's accent. We do this on the icon, NOT * on the surrounding line, because the line text needs to inherit normal * prose color — only the icon should pick up the type accent. * -------------------------------------------------------------------------- */ .cm-callout-icon-wrapper { display: inline-flex; align-items: center; /* Breathing room between the icon and the callout title text. The * wrapper contains only the icon SVG; the title is the next inline * sibling, so the gap is a margin-inline-end (not flex `gap` — `gap` * has nothing to act on with a single child). */ margin-inline-end: 0.5em; } .cm-callout-icon { vertical-align: -0.125em; flex-shrink: 0; } .cm-callout-note .cm-callout-icon { color: var(--syntax-callout-note); } .cm-callout-abstract .cm-callout-icon { color: var(--syntax-callout-abstract); } .cm-callout-info .cm-callout-icon { color: var(--syntax-callout-info); } .cm-callout-todo .cm-callout-icon { color: var(--syntax-callout-todo); } .cm-callout-tip .cm-callout-icon { color: var(--syntax-callout-tip); } .cm-callout-success .cm-callout-icon { color: var(--syntax-callout-success); } .cm-callout-question .cm-callout-icon { color: var(--syntax-callout-question); } .cm-callout-warning .cm-callout-icon { color: var(--syntax-callout-warning); } .cm-callout-failure .cm-callout-icon { color: var(--syntax-callout-failure); } .cm-callout-danger .cm-callout-icon { color: var(--syntax-callout-danger); } .cm-callout-bug .cm-callout-icon { color: var(--syntax-callout-bug); } .cm-callout-example .cm-callout-icon { color: var(--syntax-callout-example); } .cm-callout-quote .cm-callout-icon { color: var(--syntax-callout-quote); } /* ---- Plain blockquote (no [!type] header) ---- * * Applied by calloutDecoration when a `> text` line is NOT inside any * callout block. Distinguished from callouts by: * - Thinner left border (2px vs 4px) * - Muted border color (text-tertiary, not a callout palette color) * - No background tint * - Italic body text — matches the typographic convention for quoted prose * * The leading `> ` on inactive lines is already hidden via * `.cm-blockquote-formatting-hidden` (same mechanism callouts use), so the body text aligns visually with the * padding-left offset. * * Mode scope: calloutDecoration is only loaded via `livePreview()` (see * `src/editor/extensions/live-preview.ts`), which is disabled in source mode. * These styles therefore only affect live-preview and read modes. If that * wiring ever changes, the source-mode override rules in the preceding * `[data-mode="source"]` block would need to disable `.cm-quote` colors too. * -------------------------------------------------------------------------- */ .cm-quote { /* Layout (margin-left, padding-left, border-left) and the inline * accent bar all come from the shared `.cm-callout, .cm-quote` rule * above, with `--cm-bq-self-color` and `--cm-bq-bar-width` set to * plain-quote values (text-tertiary, 2px) earlier. Only the * typographic affordances live here. */ font-style: italic; color: var(--text-secondary); } /* ---- Horizontal rule (live-preview + read mode, #218) ---- * * `cm-md-hr-line` is the line class emitted by hrDecoration on every * Lezer `HorizontalRule` node (live-preview-only by virtue of the * extension being registered via livePreview()). The visible separator * is a `border-top` painted on a `::before` pseudo-element so the rule * sits in the VERTICAL CENTER of the padded line, rather than on the * top edge (where it would look attached to the previous block). * * `cm-md-hr-line-hidden` is the mark decoration over the raw `---` / * `***` / `___` text emitted on inactive lines. It uses the SAME * collapse pattern as `.cm-inline-formatting-hidden`: zero width, zero * line-height, opacity 0 — keeping a DOM presence so caret motion and * CM6's line measurement continue working while the visible glyphs * disappear and the border alone reads as the rule. * * Source mode visual treatment: NONE. The decoration extension is not * registered in source mode, and we do NOT add a source-mode-scoped * rule for `.cm-md-hr-line` here. Source mode shows raw text only. * -------------------------------------------------------------------------- */ .cm-md-hr-line { /* Line keeps its normal height so caret motion isn't surprised by a * 0-height line. The visible rule is painted on the ::before * pseudo-element below (centered between the top and bottom padding), * NOT on the line's own border edge. The hidden raw text is collapsed * by .cm-md-hr-line-hidden. * * `padding-block` (not `margin-block`) — line classes apply to * `.cm-line` elements that CM6 lays out and measures itself. Vertical * margins can shift line metrics in ways that confuse cursor * positioning. The existing line-class convention in this file uses * padding-block for breathing room (see `.cm-heading-*` and * `.cm-codeblock`). Lock that idiom for HR too. */ padding-block: 0.75em; position: relative; } /* Visible rule, centered in the padded line, ONLY when the raw * dashes are hidden. When the cursor is on the HR line the hide * mark is omitted and the dashes show; we suppress the painted * rule there so the user sees clean source (just `---`) instead of * a horizontal line striking through the dashes. * * `:has(.cm-md-hr-line-hidden)` is supported in all evergreen * browsers (Chrome 105+, Safari 15.4+, Firefox 121+). The PWA * targets these baselines. */ .cm-md-hr-line:has(.cm-md-hr-line-hidden)::before { /* - block-level so it lays out as its own row inside the .cm-line, * - top: 50% + translateY(-50%) places its top edge at the line's * vertical center, * - the border-top then sits at that center point. * This keeps the HR visually equidistant from neighboring paragraphs * regardless of the padding-block value. */ content: ""; display: block; position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); border-top: 1px solid var(--text-tertiary); } .cm-md-hr-line-hidden { /* WHOLE-LINE HIDE — see hidden-text-classes.ts idiom 2 (#269). * * The line's only visible content is the raw `---`. Zero-geometry * collapse (the previous `width:0; height:0; line-height:0`) leaves * CM6 with no caret-targetable surface — ArrowDown skips the line. * * `color: transparent` paints nothing while preserving full text * geometry; `user-select: none` keeps an invisible click+drag from * highlighting the dashes. The visible separator is painted on the * gated `.cm-md-hr-line:has(.cm-md-hr-line-hidden)::before` rule, * which already exists and is unchanged by this fix. */ color: transparent; user-select: none; } /* ---- Fenced & indented code blocks (decoration layer) ---- * * `cm-codeblock` is emitted as a line class by code-fence-decoration.ts * on every line inside a FencedCode or CodeBlock Lezer node. Inline * `code` spans (which carry `cm-md-code`) are intentionally NOT styled * here — they keep their tertiary-text monospace treatment from the * source-mode rules above. * * Visible in source AND live-preview modes (the decoration extension is * always-on, not gated behind livePreviewCompartment). Read mode keeps * the same chrome for now — we can opt out later by scoping to a * `:not([data-mode="read"])` ancestor if it proves distracting. * * Token coloring (`.tok-*`) is scoped to `.cm-codeblock` so language * tokens don't bleed into inline code, prose, or markdown source spans. * The classHighlighter installed in CM6Editor.tsx assigns the classes; * this CSS controls their appearance inside fenced blocks only. * --------------------------------------------------------------------- */ .cm-codeblock { background-color: var(--code-bg-sunken); font-family: var(--font-mono); padding-inline: 0.75em; /* Vertical breathing room: per-line padding (decoration-layer effect). * * Round-1 review #14 noted that `padding-block` applied per-`.cm-line` * contributes line-internal vertical chrome, NOT outer-block daylight, * because the next line's background tile sits flush against the * previous one. We keep this intentionally: * * 1) The actual ask was vertical breathing room. With `0.15em` per * line of top+bottom padding, each fenced line is 0.3em taller * than a prose line. Adjacent prose paragraphs already provide * ample daylight via paragraph margins. * 2) Adding outer block padding via `:first-of-type / :last-of-type` * would require knowing where blocks start/end without coupling to * the decoration's tree-walk — `.cm-line.cm-codeblock:first-of-type` * selects only the first .cm-line in the editor, not per-block. * Solving this cleanly would mean emitting a different decoration * class on the open/close fence lines, doubling the decoration * surface for marginal visual gain. * * If outer-block padding becomes a real ask later, the right next step * is to emit `cm-codeblock-first` / `cm-codeblock-last` line classes * from code-fence-decoration.ts and target those here. */ padding-block: 0.15em; } /* Token colors scoped to fenced blocks. * * Maps every `tok-*` class @lezer/highlight's `classHighlighter` emits to * a `--code-syntax-*` theme variable declared in `src/index.css`. Each * `--code-syntax-*` var defaults to a value in the prose / callout * palette so the two move together by default, but a custom theme can * override any single one to decouple. * * Near-synonyms group onto one rule (e.g., `tok-string2` joins `tok-string`) * so the rule list stays compact rather than a 30:9 repetition. * * `tok-labelName` does NOT map onto the variable color here. The language * info string on a fence (` ```typescript `) is the only span carrying * `tok-labelName` we want to style — and we want to DIM it, not paint it * the variable color. To avoid hitting in-language label tokens (e.g. C * goto labels) and accidentally dimming real code, the dim rule scopes to * spans that ALSO carry `cm-md-label` (the markdown layer's label tag, * which is uniquely the fence info string). See dedicated rule below. */ .cm-codeblock .tok-keyword { color: var(--code-syntax-keyword); } .cm-codeblock .tok-atom, .cm-codeblock .tok-bool, .cm-codeblock .tok-literal { color: var(--code-syntax-atom); } .cm-codeblock .tok-string, .cm-codeblock .tok-string2, .cm-codeblock .tok-url, .cm-codeblock .tok-link { color: var(--code-syntax-string); } .cm-codeblock .tok-number { color: var(--code-syntax-number); } .cm-codeblock .tok-comment { color: var(--code-syntax-comment); font-style: italic; } .cm-codeblock .tok-variableName, .cm-codeblock .tok-variableName2, .cm-codeblock .tok-namespace { color: var(--code-syntax-variable); } .cm-codeblock .tok-propertyName, .cm-codeblock .tok-macroName { color: var(--code-syntax-property); } /* `definition` variant of variableName/propertyName: function definitions * (`function foo(...)`) and object-literal keys (`{ foo: 1 }`). Use the * property color so callable names and member keys read consistently. */ .cm-codeblock .tok-variableName.tok-definition, .cm-codeblock .tok-propertyName.tok-definition { color: var(--code-syntax-property); } .cm-codeblock .tok-typeName, .cm-codeblock .tok-className { color: var(--code-syntax-type); } .cm-codeblock .tok-operator, .cm-codeblock .tok-punctuation { color: var(--code-syntax-operator); } .cm-codeblock .tok-meta { color: var(--code-syntax-meta); } .cm-codeblock .tok-invalid { color: var(--error); } /* Markdown-as-fenced-block constructs that the lang-markdown nested grammar * may emit when a doc has nested ` ```markdown ` fences: keep these * synchronized with the prose layer so they don't visually clash. */ .cm-codeblock .tok-heading { color: var(--code-syntax-keyword); font-weight: 600; } .cm-codeblock .tok-strong { font-weight: 700; } .cm-codeblock .tok-emphasis { font-style: italic; } /* Language label dim treatment. * * Adversary review #1: a naive `.cm-codeblock .tok-labelName` rule would * also dim real in-language label tokens (e.g. C goto labels), since * `tags.labelName` from @lezer/highlight is not unique to fence info. * Tighten the selector to only spans carrying BOTH `tok-labelName` AND * `cm-md-label` — the latter is emitted by our markdown HighlightStyle * exclusively for fence info strings, so it's a reliable discriminant. * * Adversary review #3: the existing `.cm6-editor-container[data-mode= * "source"] .cm-md-label` rule has specificity 0,2,1 with the attribute * selector. To override it in source mode we need to match or exceed — * scoping under the same `.cm6-editor-container` ancestor with the * combined `.tok-labelName.cm-md-label` brings the new rule to 0,3,0, * which beats 0,2,1. Effective in all modes (source, live-preview, read). * * Adversary review #5: dropped the 0.85em font-size shrink. Fence info * sits inline with the open ` ``` ` punctuation; mixing 1em and 0.85em * caused inconsistent line-box rhythm across monospace fallbacks. Color * + italic alone is enough to recede the label. */ .cm6-editor-container .cm-codeblock .tok-labelName.cm-md-label { color: var(--code-syntax-fence-label); font-style: italic; } .cm-editor .cm-cursor, .cm-editor .cm-dropCursor { border-left-color: var(--text-primary); border-left-width: 2px; } .cm-editor .cm-content { caret-color: var(--text-primary); } /* ---- Selection ---- */ .cm-editor .cm-selectionBackground { background: rgba(74, 158, 255, 0.2) !important; } /* Hide CM6's built-in selection overlay. Reams paints selection rects * through `domSelectionLayer` from the live DOM selection so widget and * wrapped-line geometry matches the browser's own range rects; leaving * the stock `.cm-selectionLayer` visible would double-paint a second, * geometry-divergent overlay on top. This is overlay chrome, not * content geometry. */ .cm-editor .cm-selectionBackground, .cm-editor .cm-selectionLayer .cm-selectionBackground { /* overlay chrome */ background: rgba(74, 158, 255, 0.2) !important; } .cm-editor .cm-selectionLayer .cm-selectionBackground { display: none; } .cm-editor .cm-dom-selectionLayer .cm-domSelectionBackground { background: rgba(74, 158, 255, 0.2) !important; } .cm-editor.cm-focused .cm-selectionBackground { background: rgba(74, 158, 255, 0.3) !important; } .cm-editor.cm-focused .cm-selectionBackground, .cm-editor.cm-focused .cm-dom-selectionLayer .cm-domSelectionBackground { background: rgba(74, 158, 255, 0.3) !important; } /* ---- Placeholder ---- */ .cm-editor .cm-placeholder { color: var(--text-tertiary); font-style: italic; } /* ---- Source mode: monospace font ---- */ .cm6-editor-container[data-mode="source"] .cm-scroller { font-family: var(--font-mono); } /* ---- Read mode visuals (subtle + hide comments) * * Applied whenever the container has data-mode="read". CSS-only; no CM6 * Compartment, Facet, or extension is needed for this. The decoration * extensions still run, but the read-mode container CSS hides the punctuation * and comment spans so the text reads like prose. * * Visual changes: * - Subtle bg tint (bg-secondary) to distinguish from edit mode * - Cursor and drop cursor fully hidden; caret-color:transparent prevents * the native caret from appearing if something tries to focus the content * - cm-md-punctuation hidden (headings mark, list mark, etc.) * - cm-md-comment hidden (entire block, including delimiters) * - cm-md-wikilink-bracket hidden (raw [[]] brackets when a chip isn't shown) * - cm-md-frontmatter-fence hidden (--- lines bordering frontmatter) * * CURSOR SPECIFICITY NOTE: CM6's default theme declares * `.cm-editor.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor { display: block }` * (specificity 0,4,1). Our simpler selector * `.cm6-editor-container[data-mode="read"] .cm-cursor` is 0,3,0 — losing * the cascade. We use `!important` plus a descendant match including * `.cm-cursorLayer` to guarantee override. `!important` is the idiomatic * escape hatch for overriding library defaults; acceptable here because * read mode is an explicit override of normal editor behavior. * -------------------------------------------------------------------------- */ .cm6-editor-container[data-mode="read"] { background-color: var(--bg-secondary); } /* CURSOR-SKIP JUSTIFICATION (#269): read mode has no caret — the * cursor layers are forcibly hidden so the editor never advertises an * insertion point in read-only mode. `display: none` here is safe * even though it would otherwise be flagged by the * cursor-skip-decoration audit — read mode has no caret, so the user * cannot observe a "skipped line". */ .cm6-editor-container[data-mode="read"] .cm-cursorLayer, .cm6-editor-container[data-mode="read"] .cm-cursor, .cm6-editor-container[data-mode="read"] .cm-dropCursor { display: none !important; } .cm6-editor-container[data-mode="read"] .cm-content { caret-color: transparent; } /* Firefox still shows the text/I-beam cursor on editable surfaces even when * the caret itself is hidden. Force the read-mode text surfaces back to the * default pointer so the editor no longer advertises an insertion point. */ .cm6-editor-container[data-mode="read"] .cm-editor, .cm6-editor-container[data-mode="read"] .cm-scroller, .cm6-editor-container[data-mode="read"] .cm-content, .cm6-editor-container[data-mode="read"] .cm-line { cursor: default !important; } /* CURSOR-SKIP JUSTIFICATION (#269): read mode has no caret (cursor * layers are display:none above and .cm-content carries * caret-color: transparent), so cursor-skip cannot manifest here even * though `display: none` would otherwise be flagged by the * cursor-skip-decoration audit. */ .cm6-editor-container[data-mode="read"] .cm-md-punctuation, .cm6-editor-container[data-mode="read"] .cm-md-comment, .cm6-editor-container[data-mode="read"] .cm-md-wikilink-bracket, .cm6-editor-container[data-mode="read"] .cm-md-frontmatter-fence { display: none; } /* Frontmatter body lines (between fences) are hidden in read mode — * frontmatter is metadata, not content to render as prose. * CURSOR-SKIP JUSTIFICATION (#269): read mode has no caret (see the * cursor-layer rule above), so `display: none` is safe here. */ .cm6-editor-container[data-mode="read"] .cm-md-frontmatter { display: none; } @media (max-width: 767px) { .cm-list-fold-control { opacity: 1; } } /* ---- Image embed widget ---- * * Two display modes, distinguished by the wrapper's `data-mode` attribute * (set by the widget's toDOM()): * * data-mode="inline" (cursor outside embed): * Wrapper is a emitted by Decoration.replace covering [from, to). * Must flow with surrounding paragraph text. Image is inline-block so * it can sit alongside text on the same visual line. * * data-mode="block" (cursor touches embed markup): * Wrapper is a
emitted by Decoration.widget({block:true,side:1}) * at line.to. CM6 places it on its own visual line below the embed. * The image fills the row. * * GEOMETRY NOTE: * Standalone embeds retain their full-size preview. Mixed-text images * fit inside a font-sized box so the surrounding line stays at text height. */ .cm-image-embed[data-mode="inline"] { display: inline; } .cm-image-embed[data-mode="block"] { display: block; } .cm-image-embed-loading-text { color: var(--text-tertiary); font-style: italic; } /* Error state: dashed-border chip. Specificity is bumped to beat the * data-mode selectors above (otherwise inline mode's `display: inline` * would override and the padding/border would render incorrectly). */ .cm-image-embed[data-mode="inline"].cm-image-embed-error, .cm-image-embed[data-mode="block"].cm-image-embed-error { display: inline-block; padding: 0.1em 0.5em; border: 1px dashed var(--error); color: var(--error); border-radius: 0.25em; font-size: 0.9em; } /* Image element: inline-block by default so it flows alongside text in * inline mode. In block mode we promote it to block so it fills the row. * * Standalone images (sole content of a line) render at full size. * Mixed-content images (alongside text in a paragraph) are capped at * one em in either dimension so they don't disrupt the line flow. The cap * is consistent across inline and block modes so there's no size jump * when the cursor enters/exits the embed. * (Gallery lines — 2+ images only — are handled by image-gallery-widget.) */ .cm-image-embed-img { display: inline-block; vertical-align: middle; max-width: 100%; } /* Thumbnail cap for images mixed with paragraph text */ .cm-image-embed:not([data-standalone]) > .cm-image-embed-img { max-height: 1em; max-width: 1em; width: auto; } .cm-image-embed[data-mode="block"] > .cm-image-embed-img { display: block; } .cm-fenced-code-block-processor { --dataview-surface: color-mix(in srgb, var(--bg-primary, transparent) 92%, var(--text-primary, currentColor) 8%); --dataview-row-rule: color-mix(in srgb, var(--text-primary, currentColor) 14%, transparent); --dataview-header-rule: color-mix(in srgb, var(--text-primary, currentColor) 28%, transparent); --dataview-link: var(--accent, currentColor); --dataview-focus: var(--accent, currentColor); --dataview-cell-padding: 0.42rem 0.62rem; display: block; width: min(100%, 48rem); max-width: 100%; box-sizing: border-box; min-width: 0; overflow-x: auto; overflow-y: visible; /* CM6 measures block widgets by their element box. Keep the visual breathing room inside that box so hit-testing below the widget stays aligned with the following source line. */ margin: 0; border-top: 0.55rem solid transparent; border-bottom: 0.75rem solid transparent; padding: 0.12rem 0.2rem 0.2rem; background: var(--dataview-surface); background-clip: padding-box; border-radius: 2px; line-height: 1.45; -webkit-overflow-scrolling: touch; } .cm-fenced-code-block-processor > div { min-width: 0; } /* Bases use the same table surface as .base files; other processors retain the subdued code-result backdrop above. */ .cm-fenced-code-block-processor[data-language="base"] { width: 100%; padding: 0; overflow-x: visible; background: transparent; } .cm-fenced-code-block-processor table { width: max-content; min-width: 100%; border-collapse: collapse; text-align: start; } .cm-fenced-code-block-processor th, .cm-fenced-code-block-processor td { padding: var(--dataview-cell-padding); vertical-align: top; text-align: start; } .cm-fenced-code-block-processor :is(table.dataview, table.datacore-table) > thead > tr > th { border-bottom: 1px solid var(--dataview-header-rule); font-weight: 650; white-space: nowrap; } /* Dataview appends its result count to the first table heading. Reams keeps * the header as the table's quiet label row; the count adds a second visual * line without helping scanability. The count is generated, non-editable * processor output, and the reading-view branch has no live caret, so hiding * it cannot affect editor caret geometry. */ .cm-fenced-code-block-processor table.dataview > thead .dataview.small-text, .markdown-reading-view table.dataview > thead .dataview.small-text { display: none; } .cm-fenced-code-block-processor :is(table.dataview, table.datacore-table) > tbody > tr > td { border-bottom: 1px solid var(--dataview-row-rule); } .cm-fenced-code-block-processor :is(table.dataview, table.datacore-table) > tbody > tr:last-child > td { border-bottom: 1px solid var(--dataview-row-rule); } .cm-fenced-code-block-processor :is(table.dataview, table.datacore-table) > tbody > tr:hover > td { background: color-mix(in srgb, var(--dataview-link) 6%, transparent); } /* Datacore retains its interactive components; suppress the upstream * whole-row highlight so the shared cell treatment wins in both themes. */ .cm-fenced-code-block-processor table.datacore-table > tbody > tr:hover { background-color: transparent !important; } .cm-fenced-code-block-processor .datacore-table-group-header h2 { margin-block: 0.35rem 0.22rem; font-size: inherit; font-weight: 650; } .cm-fenced-code-block-processor .dc-paging-control { margin-block: 0.45rem 0.25rem; gap: 0.4rem; } .cm-fenced-code-block-processor .dc-paging-control button { min-width: 2rem; min-height: 2rem; } .cm-fenced-code-block-processor .dc-paging-control button:focus-visible { outline: 2px solid var(--dataview-focus); outline-offset: 2px; } .cm-fenced-code-block-processor .dataview, .cm-fenced-code-block-processor .dataview.result-group, .cm-fenced-code-block-processor ul { max-width: 100%; overflow-wrap: anywhere; } .cm-fenced-code-block-processor .dataview.list-view-ul, .markdown-reading-view .dataview.list-view-ul { margin-block: 0.28rem 0.48rem; padding-inline-start: 1.4rem; line-height: 1.45; } .cm-fenced-code-block-processor .dataview.list-view-ul > li, .markdown-reading-view .dataview.list-view-ul > li { margin-block: 0 !important; padding-block: 0.08rem !important; line-height: inherit; } .cm-fenced-code-block-processor .dataview.list-view-ul > li > span > .markdown-rendered:has(> a:only-child), .markdown-reading-view .dataview.list-view-ul > li > span > .markdown-rendered:has(> a:only-child) { display: inline; } .cm-fenced-code-block-processor .dataview.list-view-ul ul, .cm-fenced-code-block-processor .dataview.list-view-ul ol, .markdown-reading-view .dataview.list-view-ul ul, .markdown-reading-view .dataview.list-view-ul ol { margin-block: 0.1rem 0.2rem; padding-inline-start: 1.35rem; line-height: inherit; } .cm-fenced-code-block-processor ul { margin-block: 0.28rem 0.48rem; padding-inline-start: 1.4rem; } .cm-fenced-code-block-processor ol { margin-block: 0.28rem 0.48rem; padding-inline-start: 1.55rem; } .cm-fenced-code-block-processor .dataview.result-group { margin-block: 0.35rem 0.65rem; } .cm-fenced-code-block-processor .result-group { margin-block: 0.35rem 0.65rem; } .markdown-reading-view .dataview .result-group { margin-block: 0.35rem 0.65rem; } .cm-fenced-code-block-processor .result-group-title { margin-block: 0 0.22rem; font-weight: 650; } .cm-fenced-code-block-processor a { color: var(--dataview-link); cursor: pointer; text-decoration-line: underline; text-decoration-thickness: 0.08em; text-underline-offset: 0.16em; } .cm-fenced-code-block-processor a:hover { text-decoration-thickness: 0.12em; } .cm-fenced-code-block-processor input[type="checkbox"] { width: 1rem; height: 1rem; margin: 0 0.38rem 0 0; accent-color: var(--dataview-link); vertical-align: -0.15em; } .cm-fenced-code-block-processor input[type="checkbox"]:focus-visible, .cm-fenced-code-block-processor a:focus-visible { outline: 2px solid var(--dataview-focus); outline-offset: 2px; } .cm-fenced-code-block-processor:focus-within { outline: 1px solid color-mix(in srgb, var(--dataview-focus) 45%, transparent); outline-offset: 2px; } .cm-fenced-code-block-processor-loading { color: var(--text-tertiary); font-style: italic; } .cm-fenced-code-block-processor-empty { color: var(--text-tertiary); font-size: 0.9em; } .cm-fenced-code-block-processor-error { padding: 0.35em 0.6em; border: 1px dashed var(--error); color: var(--error); font-size: 0.9em; } @media (max-width: 600px) { .cm-fenced-code-block-processor { width: 100%; margin-block: 0.42rem 0.6rem; padding-inline: 0.08rem; } .cm-fenced-code-block-processor th, .cm-fenced-code-block-processor td { padding: 0.34rem 0.48rem; } } /* ---- Task checkbox widget (live-preview + read mode) ---- * * Rendered by checkbox-widget.ts inside `livePreview()`. The extension * emits a Decoration.replace covering the 3-char TaskMarker ([ ] or [x]) * with an widget. * * Styling goals: * - Vertically aligned with surrounding text baseline * - Pointer cursor for discoverability * - Adequate touch target on mobile (at least 18×18 CSS px) * - No outline/border surprises in dark theme * * Read-mode cursor override: the read-mode rules above set `cursor: default` * on .cm-line. We override back to `pointer` specifically for checkboxes so * they remain discoverable as interactive elements. * -------------------------------------------------------------------------- */ .cm-task-checkbox { cursor: pointer; width: 1em; height: 1em; margin: 0 0.25em 0 0; vertical-align: middle; position: relative; top: -0.1em; accent-color: var(--accent); } .cm-task-checkbox-hitbox { position: relative; } /* Grow only the pointer target on touch layouts. The pseudo-element has no * paint, no layout footprint, and delegates its click through the wrapper. * The wrapper deliberately has no dimensions or margins: the input retains * the exact inline geometry it had before the hit-target enhancement. */ @media (hover: none) { .cm-task-checkbox-hitbox::before { content: ""; position: absolute; inset: -4px; } } /* In read mode, restore pointer cursor on checkboxes despite the global * `cursor: default !important` on .cm-line. The !important and extra * specificity are needed to override the read-mode rule. */ .cm6-editor-container[data-mode="read"] .cm-task-checkbox { cursor: pointer !important; } /* Read mode pointer overrides: read mode globally forces * `cursor: default !important;` on `.cm-line` and friends, but link * affordances must remain pointer-cursored to match the browser default * and the user's tap intent. Mirrors the `.cm-task-checkbox` override * pattern directly above. The external-link icon is included because * it's a clickable affordance (`.cm-md-external-link-icon`'s * domEventHandlers branch resolves icon clicks to the adjacent URL); * without this override, mobile read-mode users would see no cursor * affordance over the icon despite it being clickable. */ .cm6-editor-container[data-mode="read"] .cm-md-inline-link-label, .cm6-editor-container[data-mode="read"] .cm-md-bare-url, .cm6-editor-container[data-mode="read"] .cm-md-external-link-icon, .cm6-editor-container[data-mode="read"] .cm-wikilink-chip { cursor: pointer !important; } /* ---- List bullet markers (live-preview + read mode) ---- * Rendered by list-bullet-widget.ts. Marks the 1-char ListMark so the * source marker stays in the DOM with its original width. A non-editable overlay * paints a measurable depth-specific glyph over the marker anchor, keeping * editable source geometry separate from visual marker geometry. * * Depth cycle (3-cycle): disc (•) → triangle (▸) → square (▪). * Filled glyphs all the way down: hollow shapes (◦) read as "empty" or * "incomplete" in checklist contexts and visually compete with task * checkbox affordances on `- [ ] item` lines. Filled disc / triangle / * square give clean nesting differentiation without that semantic clash. * -------------------------------------------------------------------------- */ .cm-line.cm-list-item { /* The source indentation belongs to markdown syntax, not the rendered * hierarchy. Move the first visual row back by that source prefix plus the * marker gutter, then leave every wrapped row at the text column. This is a * real hanging indent rather than a cosmetic offset, so long content stays * inside its own depth on narrow screens as well as desktop. */ --cm-list-gutter: 2ch; padding-left: calc(6px + var(--cm-list-offset) + var(--cm-list-gutter)); text-indent: calc(0px - var(--cm-list-offset) - var(--cm-list-gutter)); position: relative; } .cm-line.cm-list-task-item { /* A task marker becomes a 1em checkbox plus its inline spacing, so it needs * a wider hanging gutter than a plain bullet. */ --cm-list-gutter: 3.5ch; } /* Completion is semantic feedback, not a source-mode transformation. The * checkbox decoration only exists in live-preview/read mode, so source text * remains completely raw. The completion pulse itself is driven by WAAPI in * checkbox-widget.ts and is intentionally live-preview-only. */ .cm-task-completed { color: var(--text-tertiary); text-decoration-line: line-through; text-decoration-thickness: 1px; text-decoration-color: currentColor; /* A two-stop, double-width background lets WAAPI reveal the completed * color from left to right in lockstep with the pseudo-element strike. */ background-image: linear-gradient(to right, var(--text-tertiary) 50%, var(--text-primary) 50%); background-size: 200% 100%; background-position: 0 0; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .cm-list-source-indent { display: inline-block; width: var(--cm-list-offset); max-width: var(--cm-list-offset); color: transparent; white-space: pre; user-select: none; } .cm-list-bullet { display: inline-flex; justify-content: center; position: relative; width: 1ch; color: transparent; } /* The active marker retains its editable source glyph on the cursor item; * ancestor markers keep their rendered glyph, but share the accent so the * path reads as one connected editing context. */ .cm-list-bullet-source { color: var(--text-tertiary); } .cm-list-bullet-source.cm-list-bullet-source-active { color: var(--accent); } .cm-list-bullet-glyph.cm-list-bullet-glyph-active { color: var(--accent); transition: color 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1); } .cm-list-bullet-glyph-record.cm-list-bullet-glyph-active { opacity: 1; } /* Measured in list-bullet-widget.ts from the active unordered/task ancestry. * It sits behind editable content and uses an L-shaped route so each parent * rail remains vertical until it joins the deeper bullet. */ .cm-list-thread { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; /* The editor content establishes its own paint layer. Keep the thread in * the glyph layer so its animated bridge is painted above that content, * while DOM order still leaves bullet glyphs on top of the line. */ z-index: 1; } .cm-list-thread-path { fill: none; stroke: var(--accent); stroke-linecap: round; stroke-linejoin: round; stroke-opacity: 0.72; stroke-width: 1.5px; } .cm-list-thread-bridge { stroke-opacity: 0.52; stroke-width: 2px; } @media (prefers-reduced-motion: reduce) { .cm-list-bullet-glyph-active, .cm-list-thread-path { transition: none; } } /* Browser-measurable glyphs live outside `.cm-content`, preserving native * selection geometry while the source marker continues to own editable width. */ .cm-list-bullet-glyph-layer { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; z-index: 1; } /* Local-only list folding. The source stays mounted for CM6's position and * selection model; descendant rows animate their own visual height away. */ .cm-line.cm-list-folded-child { max-height: 0; min-height: 0; padding-block: 0; opacity: 0; overflow: hidden; } .cm-list-fold-control-layer { position: absolute; overflow: visible; pointer-events: none; z-index: 2; } .cm-content { --cm-list-gutter: 2ch; } .cm-list-fold-control { width: 2rem; height: 2rem; margin: 0; padding: 0; border: 0; border-radius: 0.25rem; appearance: none; background: transparent; color: var(--text-tertiary); cursor: pointer; font: inherit; font-size: 1.25em; line-height: 1; display: grid; place-items: center; pointer-events: auto; touch-action: manipulation; will-change: transform; transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), color 140ms ease-out, background-color 140ms ease-out; } .cm-list-fold-control { position: absolute; /* `left`/`top` are the measured glyph center. The translate below centers * this box on that point; an additional negative margin double-shifts it * upward and left, especially obvious on top-level items. */ transform: translate(-50%, -50%) rotate(90deg); } .cm-list-fold-control[data-collapsed="true"] { transform: translate(-50%, -50%) rotate(0deg); } @media (prefers-reduced-motion: reduce) { .cm-list-fold-control { transition: none; } } .cm-list-bullet-glyph { display: block; position: absolute; width: max-content; color: var(--text-tertiary); line-height: 1; pointer-events: none; white-space: nowrap; transform: translate(-50%, -50%); text-indent: 0; } .cm-list-bullet-glyph-record { display: grid; place-items: center; width: 1.5rem; height: 1.5rem; padding: 0; border: 0; background: transparent; color: color-mix(in srgb, var(--text-tertiary) 82%, var(--bg-primary)); cursor: pointer; font: inherit; font-size: 1em; pointer-events: auto; touch-action: manipulation; } /* Keep the editable source marker quiet; the disclosure glyph itself carries * active-line state without making the Markdown delimiter compete with it. */ .cm-list-bullet-source.cm-list-bullet-record, .cm-list-bullet-source.cm-list-bullet-record.cm-list-bullet-source-active { color: color-mix(in srgb, var(--text-tertiary) 82%, var(--bg-primary)); } .cm-list-bullet-glyph-record[data-active="true"] { color: var(--accent); } .cm-list-bullet-glyph-record:hover { color: var(--accent); } @media (pointer: coarse) { .cm-list-bullet-glyph-record { width: 2.25rem; height: 2.25rem; } } .cm-list-bullet-glyph-disc { font-size: 1.2em; } /* Triangle uses U+25B8 BLACK RIGHT-POINTING SMALL TRIANGLE — points into * the nested content, reinforcing the "this contains a sub-list" reading. * Sized smaller than disc since the triangle's bounding box is taller. */ .cm-list-bullet-glyph-triangle { font-size: 0.95em; } .cm-list-bullet-glyph-square { font-size: 0.8em; } /* Mobile list markers need a little more optical weight against touch-first * chrome; desktop proportions remain unchanged. */ @media (max-width: 767px) { .cm-list-bullet-glyph-disc { font-size: 1.32em; } .cm-list-bullet-glyph-triangle { font-size: 1.05em; } .cm-list-bullet-glyph-square { font-size: 0.9em; } } /* ---- GFM tables (live-preview + read mode) ---------------------------- */ /* Rendered by table-grid.ts inside livePreview(). Each row line is a flex * container; each cm-table-cell wrapper is `flex: 1 1 0` so columns share * the row width equally. Pipes are hidden via cm-table-pipe-hidden * (zero-width flex item with `color: transparent`; see #269); the * separator row's `|---|---|` source text is wrapped by a * `Decoration.mark` carrying `cm-table-separator-anchor` (NOT a * `Decoration.replace`) — keeping the delimiter in the DOM as * caret-targetable inline content is what makes the line reachable * by ArrowUp/ArrowDown. CSS makes the glyphs visually invisible. * --cm-table-cols is still emitted on each row line as a parity / * future-styling hook (e.g. media queries that retarget per column count) * but flex distribution does not consume it. * * NOTE (Spike R4a / Fallback C): we use display:flex, NOT display:grid. * Spike R4a showed `display: grid` on a `.cm-line` makes * `view.coordsAtPos(offsetInside)` return null in jsdom — breaking mouse * click positioning and vertical motion. Flex preserves CM6's coordsAtPos * line-box assumptions. Visual outcome (uniformly distributed columns * with per-cell alignment) is identical for v1 layout requirements. */ /* CRITICAL specificity note: the `.cm-line.cm-table-row` compound * selector is REQUIRED. CM6's themed default `.ͼN .cm-line { display: * block }` (where ͼN is an autogenerated theme class) has specificity * 0,2,0. A bare `.cm-table-row` rule has 0,1,0 and silently loses, * leaving `display: block` on the row — which makes cells stack * vertically (each cell takes 100% width on its own visual line). The * compound `.cm-line.cm-table-row` is 0,2,0, matching CM6's own rule; * appearing later in the cascade, our rule wins. * * This was caught only by live-browser inspection — jsdom doesn't * apply the themed default, so the original spike R4 / R5 tests pass * regardless of the bug. The DOM-level integration tests in * table-grid.test.ts also pass because they only count * `.cm-table-cell` query matches, not their layout. The fix is * idiomatic CSS specificity, not a structural change. */ .cm-line.cm-table-row { display: flex; /* Explicit horizontal layout, no wrapping. Forces cells onto a * single horizontal axis regardless of viewport width or content. */ flex-direction: row; flex-wrap: nowrap; /* `nowrap` here applies to text directly in the row container (e.g., * stray text nodes between spans). Cells override back to `normal` * so long content inside a cell wraps within its cell, not pushing * the cell wider than its flex basis. */ white-space: nowrap; /* Span full content width regardless of cell sizes. Without explicit * `width: 100%` a flex container shrinks to its content. */ width: 100%; box-sizing: border-box; border-left: 1px solid var(--text-tertiary); border-right: 1px solid var(--text-tertiary); /* Inter-row horizontal divider. Without this, adjacent body rows * render as a single continuous block (no visible boundary between * them), making a 4-row table look like 1 row with multi-line cells. * Every row gets `border-bottom`, including the last row — which is * what visually closes the table on the bottom edge. */ border-bottom: 1px solid var(--text-tertiary); padding: 0; } /* The separator row's `|---|---|` content is masked by the * cm-table-separator-anchor mark below (#269). Its `border-bottom` * would render as a thin extra line above the body row's own * `border-top` — remove it so the visible separator is just the * meeting of header `border-bottom` + body `border-top`. */ .cm-line.cm-table-row.cm-table-separator-row { border-bottom: 0; } /* The first row needs an explicit `border-top` since the row container's * `border-bottom` only handles the bottom edge; the header would * otherwise be unbordered on top. The last row's bottom edge already * comes from the shared `.cm-line.cm-table-row` rule above (no separate * rule needed). */ .cm-table-row-first { border-top: 1px solid var(--text-tertiary); } .cm-table-header-row { font-weight: 600; background-color: var(--code-bg-sunken); } .cm-table-separator-row { /* WHOLE-LINE HIDE — see hidden-text-classes.ts idiom 2 (#269). * * VISUAL: the separator row collapses to zero height so the * header's `border-bottom` and body's `border-top` meet directly * with no visible gap (matches the pre-#269 visual layout). The * row's `.cm-line` and its child anchor span still exist in the * DOM but render with zero geometry. * * CARET-TARGETABILITY: caretRangeFromPoint cannot find a zero- * geometry line, but that is fine here because the cursor never * arrives at the separator via column-Y projection — the * table-aware ArrowUp/ArrowDown keymap (table-keybindings.ts) routes * directly to `state.doc.lineAt(struct.separator.from).from` via * `view.dispatch({selection})`, which bypasses geometry-based * placement entirely. So the line is reachable as a navigation stop * even though it is visually zero-height. * * The separator's `|---|---|` text is wrapped by a Decoration.mark * carrying `.cm-table-separator-anchor`. The mark keeps the source * text in the DOM (allowing the user to delete or edit it), and * the same zero-geometry treatment applies — `font-size: 0; * line-height: 0; overflow: hidden` on the anchor below. * * Side-effect: the marked text remains editable. Typing into the * separator demotes the table to a paragraph (consistent with how * HR `---` behaves in live-preview). * * Side pipes are dropped (`border-left: 0; border-right: 0`) so * the otherwise-blank separator row doesn't render boxed-in. */ min-height: 0; height: 0; line-height: 0; padding: 0; margin: 0; overflow: hidden; border-left: 0; border-right: 0; user-select: none; } /* CURSOR-SKIP JUSTIFICATION (#269 idiom 2): the separator-row text * is wrapped by a Decoration.mark; CSS hides the `|---|---|` glyphs * visually. Geometry is zero-collapsed (`height: 0; line-height: 0; * font-size: 0; overflow: hidden`) so the row renders as a zero-px * stripe and the header `border-bottom` meets the body `border-top` * with no visible gap. The mark is caret-targetable as a navigation * stop because the table-aware ArrowUp/ArrowDown keymap (see * table-keybindings.ts) routes the cursor here via * `view.dispatch({selection})`, which does not require geometry — * not via column-Y projection through caretRangeFromPoint, which * would fail on a zero-geometry box. */ .cm-table-separator-anchor { display: block; height: 0; line-height: 0; font-size: 0; overflow: hidden; color: transparent; user-select: none; } .cm-table-pipe-hidden { /* WHOLE-LINE-PARTICIPANT HIDE — see hidden-text-classes.ts idiom 2 (#269). * * Pipes interleave with cell content on a row line. Pre-fix * `display: none` removed pipe characters from DOM layout entirely, * so coordsAtPos for offsets landing on pipes returned null and * arrow-key navigation could not find the row. * * Post-fix: keep pipes as zero-width flex items * (`flex: 0 0 0; min-width: 0; width: 0`) with `color: transparent` * and `user-select: none`. They remain in flex flow so CM6 resolves * coordinates, but consume no visual space. The cell-mark wrapper * around inter-pipe content still produces the single-flex-item-per-cell * invariant table-grid.ts depends on. */ flex: 0 0 0; min-width: 0; width: 0; color: transparent; user-select: none; /* No `display: none` — see comment above. */ } /* Each cell wrapper takes 1/N of the row width. * * `display: block` is critical: `` elements default to inline, * and although flex items are normally blockified by the flex layout * algorithm, some browsers (and various interactions with default * `` line-height and CM6's measure pass) leave them at intrinsic * inline-content size. Explicitly making the cell a block element * avoids that footgun. * * `flex-basis: 0%` (NOT `0`) is the spec-compliant value for "ignore * content size when distributing space" — `flex: 1 1 0` is interpreted * as `0%` by the spec but historical browser quirks have parsed * unitless `0` as `0px`-with-content-fallback. The explicit `0%` is * more reliable. * * `min-width: 0` lets long content (wider than its share) shrink rather * than push the cell wider than the basis. */ .cm-table-cell { display: block; flex: 1 1 0%; min-width: 0; padding: 0.25em 0.5em; border-right: 1px solid var(--text-tertiary); overflow: hidden; /* Override the row's `white-space: nowrap` so long cell content * wraps within the cell rather than overflowing horizontally. */ white-space: normal; word-break: break-word; box-sizing: border-box; } .cm-table-cell-align-left { text-align: left; } .cm-table-cell-align-center { text-align: center; } .cm-table-cell-align-right { text-align: right; } /* Empty cells: the wrapping cm-table-cell mark contributes the flex item; * a CSS ::before with non-breaking space provides line height since the * mark itself wraps no visible chars. */ .cm-table-cell-empty::before { content: '\00a0'; } /* Zero-width anchor span emitted inside empty cells. For empty cells with * a whitespace gap, the wrapping cm-table-cell-empty mark already * contributes the flex item — the anchor is just a DOM marker. * For empty cells with NO gap (e.g. ||), the anchor itself becomes the * flex item; flex: 1 1 0 makes it consume its column's width and the * nbsp ::before keeps line height. */ .cm-table-cell-empty-anchor { display: block; flex: 1 1 0%; min-width: 0; box-sizing: border-box; } .cm-table-cell-empty-anchor::before { content: '\00a0'; } /* ---- Footnote ref + def (live-preview + read mode) ---- * * Refs render as inline superscript chips via Decoration.replace * widgets emitted by footnote-decoration.ts. Defs are line-decorated * (`Decoration.line` adds .cm-md-footnote-def to the line) — raw * `[^id]: body` text stays visible; CSS provides the visual grouping * (smaller font, left rule, dimmed) without hiding source. * * Source mode: the extension is mounted only via livePreview(), so * source mode mounts a different compartment without it and shows * raw `[^id]` / `[^id]: body` text uncolored by chrome. * * Read mode: same DOM/decorations as live-preview. The chip is a * `` element with cursor: pointer; the read-mode rule overrides * the global `cursor: default !important` on .cm-line for resolved * refs only (unresolved refs are not clickable so they keep default * cursor). * -------------------------------------------------------------------------- */ /* CURSOR-SKIP JUSTIFICATION (#269): Footnote refs sit in inline flow * with visible siblings (the surrounding paragraph text). The * `line-height: 0` here is for visual baseline trim, not whole-line * collapse. */ .cm-md-footnote-ref { display: inline; /* The widget element is a . The UA stylesheet renders with * `font-size: smaller` (about 0.83x) and `vertical-align: super`. We * pin explicit values here for theme-stability — guarantees the chip * is the same size across themes that override `sup`, NOT to match * the UA default. 0.75em is intentionally smaller than UA-smaller so * the chip reads as a discrete badge. */ font-size: 0.75em; vertical-align: super; /* line-height: 0 prevents the superscript from forcing the surrounding * line box taller — same reasoning as .cm-inline-formatting-hidden * above. */ line-height: 0; color: var(--accent); cursor: pointer; text-decoration: none; user-select: none; } /* Hover only on resolved chips; unresolved chips are non-clickable so * we exclude them from underline-on-hover. One rule with a :not() * selector replaces two cascading rules. */ .cm-md-footnote-ref:hover:not(.cm-md-footnote-unresolved) { text-decoration: underline; } /* Unresolved variant: muted + italic. Cursor is default (not a pointer) * because clicking does nothing — the ref has no def to scroll to. */ .cm-md-footnote-ref.cm-md-footnote-unresolved { color: var(--text-tertiary); font-style: italic; cursor: default; } /* Footnote def prefix marker (`[^id]:`). Discoverable click affordance * for def → ref navigation. The whole def line is dimmed via * .cm-md-footnote-def; the prefix is colored with the accent + dotted * underline on hover so users see "this is clickable, the body isn't". */ .cm-md-footnote-def-marker { color: var(--accent); cursor: pointer; } .cm-md-footnote-def-marker:hover { text-decoration: underline dotted; } /* Definition lines: line decoration. Smaller font, left rule, dimmed. * The leading `[^id]:` prefix gets cursor:pointer in the read-mode and * live-preview overrides below — but only on the prefix, not the body. * That prefix-only click target is enforced in the JS click handler * (footnote-decoration.ts) which checks `posAtDOM` against the def's * recorded prefixEndOffset. * * No `display: inline-block` or other layout changes — the line stays * a normal block flow element. CM6's .cm-line is `display: block` by * default, which we keep. */ .cm-md-footnote-def { font-size: 0.85em; color: var(--text-secondary); border-left: 2px solid var(--text-tertiary); padding-left: 0.5em; } /* Read-mode override: resolved ref chips remain clickable (pointer * cursor) despite the global .cm-line { cursor: default !important } * applied in the read-mode block above. Unresolved refs stay default. */ .cm6-editor-container[data-mode="read"] .cm-md-footnote-ref:not(.cm-md-footnote-unresolved) { cursor: pointer !important; } /* ---- Image gallery widget (2+ embeds on same line) ---- * * Layout is adaptive — a ResizeObserver in the widget JS toggles between * cm-image-gallery-grid (side-by-side CSS grid) and cm-image-gallery-carousel * (horizontal-scroll flex) based on whether images can fit at their minimum * width (200px each). No hard-coded breakpoints or image-count thresholds. * * Near/far mode: * - Block mode (cursor on line):
wrapper, raw text visible above * - Inline mode (cursor elsewhere): wrapper replacing embed text * -------------------------------------------------------------------------- */ .cm-image-gallery { display: block; width: 100%; box-sizing: border-box; position: relative; } .cm-image-gallery[data-mode="inline"] { display: inline-block; vertical-align: top; } /* Items container — shared by both layouts */ .cm-image-gallery-items { width: 100%; } /* Grid layout (images fit side-by-side) */ .cm-image-gallery-grid .cm-image-gallery-items { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem; } /* Carousel layout (images too wide to fit; horizontal scroll) */ .cm-image-gallery-carousel .cm-image-gallery-items { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; gap: 0.5rem; padding-bottom: 0.5rem; scrollbar-width: thin; } .cm-image-gallery-carousel .cm-image-gallery-item { flex: 0 0 200px; scroll-snap-align: start; } /* Scrollbar styling for carousel */ .cm-image-gallery-carousel .cm-image-gallery-items::-webkit-scrollbar { height: 4px; } .cm-image-gallery-carousel .cm-image-gallery-items::-webkit-scrollbar-track { background: var(--bg-secondary, #f0f0f0); } .cm-image-gallery-carousel .cm-image-gallery-items::-webkit-scrollbar-thumb { background: var(--text-tertiary, #999); border-radius: 2px; } /* Navigation buttons — always in the DOM, hidden for grid, shown for carousel. * The ResizeObserver swaps the class, so buttons appear/disappear automatically. * * CURSOR-SKIP JUSTIFICATION (#269): widget chrome — carousel nav button. * Hidden by default; shown via a sibling-state class. The line containing * the gallery widget has other visible content, so cursor-skip cannot * manifest. */ .cm-image-gallery-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 1; width: 2rem; height: 2rem; border: none; border-radius: 50%; background: var(--bg-primary); color: var(--text-primary); font-size: 1.25rem; cursor: pointer; opacity: 0; transition: opacity 0.2s ease; display: none; /* Hidden for grid; shown for carousel */ align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } /* Show nav buttons only in carousel mode */ .cm-image-gallery-carousel .cm-image-gallery-nav { display: flex; } /* Show nav on hover (desktop) and always on touch (no hover capability) */ .cm-image-gallery-carousel:hover .cm-image-gallery-nav { opacity: 0.8; } @media (hover: none) { .cm-image-gallery-carousel .cm-image-gallery-nav { opacity: 0.6; } } .cm-image-gallery-nav:hover { opacity: 1 !important; background: var(--accent); color: #fff; } .cm-image-gallery-nav-prev { left: 0.25rem; } .cm-image-gallery-nav-next { right: 0.25rem; } /* Gallery items */ .cm-image-gallery-item { margin: 0; padding: 0; text-align: center; } .cm-image-gallery-img { display: block; width: 100%; height: auto; object-fit: contain; border-radius: 0.25rem; } .cm-image-gallery-caption { margin-top: 0.25rem; font-size: 0.85em; color: var(--text-secondary); font-style: italic; } /* Loading state */ .cm-image-gallery-item-loading-text { display: block; padding: 1rem; color: var(--text-tertiary); font-style: italic; } /* Error state */ .cm-image-gallery-item-error { padding: 0.5rem; border: 1px dashed var(--error); border-radius: 0.25rem; color: var(--error); font-size: 0.85em; } /* ---- Unified frontmatter properties widget (live preview) ---- * * Rendered by root-frontmatter-widget.tsx via a StateField that block-replaces * the entire frontmatter range `[0, endOffset)` with a structured * properties table. CM6Editor excludes the extension in source mode. * * Mobile: layout is flex with `flex-wrap` on the value cell so chips * wrap naturally when narrow. No media queries needed for v1. * -------------------------------------------------------------------------- */ /* No outer border / background / fixed corner radius. The widget sits * at the top of the doc; the visual context (under the title, before * the body) is enough boundary on its own. The earlier card-style * frame was visual clutter that didn't earn its weight. Margin only — * a small bottom gap separates the properties block from the body. */ .cm-frontmatter-properties-table { display: block; width: 100%; margin: 0 0 1em; font-family: var(--font-text, inherit); /* #282: load-bearing rule is `overflow: hidden` — it clips child * overflow at the table boundary so wrapped rows stay inside the * editor instead of leaking past it and triggering whole-page * horizontal scroll on phones (<768px viewport). * * `max-width: 100%` is a defensive belt-and-suspenders cap. A * default block-level element with `width: auto` already fills * its parent's content box, so this is mostly redundant — but it * cheaply protects against a future change where the widget's * containing element gains an unexpected intrinsic-sizing path * (e.g. a CM6 contributor wraps the widget in `display: inline-block` * or `width: max-content`), in which case `max-width: 100%` would * still cap it at the parent width. */ overflow: hidden; max-width: 100%; box-sizing: border-box; } /* Per-row layout. No bottom-border separator: rows read clearly * because each row's controls are aligned and the value column * provides its own visual rhythm. * * `align-items: flex-start` (#278, D7 round-3): when a value cell * wraps to multiple lines (e.g. a tags row with many chips on a narrow * viewport), `align-items: center` would center the icon and key input * against the wrapped cell's full height — they'd visibly drift down. * Anchoring everything to the row's top edge keeps the icon, key, and * row-× lined up with the value cell's first line. * * Per-child alignment refines this further: the icon uses * `align-self: baseline` so its glyph baselines against the key * input's text baseline (the icon is intentionally smaller, 0.9em, * which under raw flex-start would sit visibly above the key text). * Inputs and the checkbox sit at the row's top edge naturally. */ .cm-frontmatter-prop-row { display: flex; align-items: flex-start; gap: 0.2em; padding: 0; /* #282: clip flex children so wrapped value content stays bounded by * the row instead of stretching the editor wider than the viewport. */ overflow: hidden; } /* Type icon — chromeless. A small muted glyph that signals the * property type at a glance. Fixed-width column so keys stay * vertically aligned across rows regardless of which glyph each * row uses. No border, background, or hover effect; the icon is * purely informational, not interactive. * * `align-self: baseline` (D7 round-3): pairs with the row's * `align-items: flex-start` to put the icon's text baseline on the * key input's text baseline — without this the smaller-font icon * would appear to sit higher than the key when both are anchored to * the row top. */ .cm-frontmatter-prop-icon { display: inline-flex; align-items: center; justify-content: center; align-self: baseline; width: 1.25em; flex: 0 0 auto; color: var(--text-tertiary); font-size: 0.9em; user-select: none; } .cm-frontmatter-prop-icon svg { width: 0.78em; height: 0.78em; } .cm-frontmatter-prop-icon-svg { align-self: center; height: 1.4em; line-height: 1; } .cm-frontmatter-prop-row[data-type="date"]:has(.reams-frontmatter-date-error) > .cm-frontmatter-prop-icon-svg, .cm-frontmatter-prop-row[data-type="datetime"]:has(.reams-frontmatter-date-error) > .cm-frontmatter-prop-icon-svg { align-self: start; margin-block-start: 0.26em; } /* Inline-editable key. Renders like a static label by default — * borderless, transparent background — and gains a focus ring on * focus to signal editability. * * Mobile-first sizing: the key column is narrow by default (4em ≈ * ~64px) so the value cell gets the lion's share of the row on a * ~360px-wide phone. Desktop widens it to 8em via the min-width * media query at the bottom of this section, restoring the wider * column for at-a-glance scanning. The 4em floor still fits typical * short Obsidian/Reams keys (`tags`, `date`) without truncation * — anything longer ellipsizes via the overflow rules below. */ .cm-frontmatter-prop-key { flex: 0 0 4em; width: 4em; padding: 0 0.25em; background-color: transparent; color: var(--text-secondary); border: 1px solid transparent; border-radius: 2px; font: inherit; /* Long keys: truncate rather than wrap — keeps row height predictable. */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cm-frontmatter-prop-key:hover:not(:disabled) { border-color: var(--text-tertiary); } .cm-frontmatter-prop-key:focus { outline: none; border-color: var(--accent); background-color: var(--bg-primary, transparent); color: var(--text-primary); } .cm-frontmatter-prop-key-readonly { flex: 0 0 4em; width: 4em; min-width: 0; padding: 0 0.25em; color: var(--text-secondary); overflow-wrap: anywhere; } .cm-frontmatter-prop-value { flex: 1 1 auto; display: flex; /* Full reflow: values wrap inside the row instead of creating a nested * horizontal-scroll lane. This makes tags and list items all visible at * once and lets long controls consume additional vertical space rather * than stealing horizontal swipe gestures. */ flex-wrap: wrap; align-items: flex-start; gap: 0.25em; row-gap: 0.25em; min-width: 0; } /* Generic input (text, number, date, datetime). Borderless by default * so the row reads as a continuous strip; focus ring on the input * itself signals editability without per-cell chrome. * * Reflow mode uses full-width controls, so long values take vertical * space instead of creating a horizontal scroll surface. */ .cm-frontmatter-prop-input { flex: 1 1 12em; width: 100%; min-width: 0; max-width: 100%; padding: 0 0.35em; background-color: transparent; color: var(--text-primary); border: 1px solid transparent; border-radius: 2px; font: inherit; } .cm-root-frontmatter-surface .cm-frontmatter-prop-input:not(textarea), .cm-root-frontmatter-surface .cm-frontmatter-prop-value > select, .cm-root-frontmatter-surface .reams-input-suggest > input, .cm-root-frontmatter-surface .reams-root-legacy-add-form > input, .cm-root-frontmatter-surface .reams-root-legacy-add-form > select { height: var(--reams-frontmatter-control-height); min-height: var(--reams-frontmatter-control-height); box-sizing: border-box; } .cm-frontmatter-prop-input:hover:not(:disabled) { border-color: var(--text-tertiary); } .cm-frontmatter-prop-input:focus { outline: none; border-color: var(--accent); background-color: var(--bg-primary, transparent); } .cm-frontmatter-prop-readonly-value { min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; } .cm-frontmatter-prop-checkbox { /* Match the in-editor task checkbox sizing; the accent-color is the * same so toggle visuals are consistent across the editor. */ width: 1em; height: 1em; margin: 0; accent-color: var(--accent); cursor: pointer; align-self: center; } .cm-frontmatter-prop-row[data-type="checkbox"] .cm-frontmatter-prop-value { align-items: center; min-height: var(--reams-frontmatter-control-height, 1.4em); } /* List/Tags chips reflow within the value lane so the full set remains * visible at once instead of requiring horizontal scrolling. */ .cm-frontmatter-prop-list { flex-wrap: wrap; } /* Chip — chromeless. The wrapper exists only to host the editable * input + the (×) hover button; visually each chip reads as a bare * value, separated from siblings by the value cell's flex `gap`. * Tag chips opt back into chrome via `.cm-frontmatter-tag-chip` * (color-mixed accent background + leading `#`). */ .cm-frontmatter-prop-chip { display: inline-flex; align-items: center; gap: 0.15em; min-width: 0; max-width: 100%; padding: 0; color: var(--text-primary); user-select: none; /* Keep chips at their content width while allowing the parent value lane * to wrap them onto additional lines. */ flex: 0 0 auto; } .cm-root-frontmatter-surface .cm-frontmatter-prop-chip { height: var(--reams-frontmatter-control-height); min-height: var(--reams-frontmatter-control-height); box-sizing: border-box; } /* Applied Types use the same colored chip as Records and the Types browser. */ .cm-root-frontmatter-surface .cm-frontmatter-prop-chip.reams-type-chip { gap: 0.125rem; padding: 2px 2px 2px 6px; color: var(--reams-type-chip-text); } .reams-root-type-chip > .cm-frontmatter-prop-chip-readonly-value { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; } .reams-root-type-chip > .cm-frontmatter-prop-chip-remove { flex: 0 0 auto; } /* The chip's editable text input. Auto-sized by `field-sizing: content` * where supported; its HTML `size` attribute provides the fallback. * * No padding: the chip hugs its text exactly. The wrapper's flex * `gap` (0.15em) provides the only space between the input and the ×. * */ .cm-frontmatter-prop-chip-input { /* No internal padding — the chip's content (input + ×) hugs the * input edges. Breathing room from the chip's left edge (when the * chip has a tinted background, e.g. tag chips) is provided by * `padding-left` on the WRAPPER (.cm-frontmatter-tag-chip), not * here, so that plain chrome-less list chips have no spurious * leading space. */ padding: 0; margin: 0; background-color: transparent; color: inherit; border: none; outline: none; font: inherit; /* Keep box-sizing explicit so a global reset can't surprise us. */ box-sizing: content-box; /* `field-sizing: content` makes the input grow/shrink to fit its * text content with no manual measurement (Chrome 123+, Safari * 17.4+, Firefox 123+). Older browsers use the input's `size`. */ field-sizing: content; /* Clamp each chip to the row width so a single unbreakable token does not * escape the wrapped layout and get hard-clipped by ancestor overflow. */ min-width: 0; min-inline-size: 1ch; max-width: 100%; } .cm-frontmatter-prop-chip-input:focus { outline: none; } /* Size editable chip values from their rendered text instead of the input's * character-count `size` heuristic, which leaves large gaps in proportional * fonts on browsers without `field-sizing: content`. */ .cm-frontmatter-prop-chip-input-sizer { display: inline-grid; min-width: 1ch; } .cm-frontmatter-prop-chip-input-sizer::after, .cm-frontmatter-prop-chip-input-sizer > .cm-frontmatter-prop-chip-input { grid-area: 1 / 1; } .cm-frontmatter-prop-chip-input-sizer::after { content: attr(data-value); visibility: hidden; white-space: pre; } .cm-frontmatter-prop-chip-input-sizer > .cm-frontmatter-prop-chip-input { width: 100%; } .cm-frontmatter-prop-chip-trailing .cm-frontmatter-prop-chip-input { min-inline-size: 3ch; } /* Suggestion inputs normally use the full row-control height. Inside the * padded add chip, match the intrinsic height of existing chip inputs. */ .cm-root-frontmatter-surface .cm-frontmatter-prop-chip-trailing .reams-input-suggest { flex: 0 0 3.5ch; width: 3.5ch; } .cm-root-frontmatter-surface .cm-frontmatter-prop-chip-trailing .reams-input-suggest > .cm-frontmatter-prop-chip-input { width: 3.5ch; height: auto; min-height: 0; padding-inline: 0; } .cm-root-frontmatter-surface .reams-record-readonly-value { padding-inline-start: var(--reams-frontmatter-value-inset); color: var(--text-tertiary); } .cm-frontmatter-prop-chip-readonly-value { min-width: 0; overflow-wrap: anywhere; } /* Trailing "+ add" empty chip discoverability (#280). The chrome-strip * pass left the trailing chip with no visual affordance beyond the * browser-default placeholder, which on most platforms renders very * faintly. On an empty list (`items: []`) the slot was essentially * invisible. * * Two reinforcements (squint test: should read as "add slot" without * focus): * 1. Explicit placeholder color — pin to text-tertiary + italic so * the `+ add` text is consistent across browsers and clearly * muted-but-visible. * 2. Leading `+` glyph rendered via ::before on the wrapper — same * mechanism the tag chip uses for its `#` glyph. Plain (non-tag) * trailing chips get an opacity-0.5 plus to indicate "add"; tag * trailing chips also get the plus (overriding the suppressed * `#` for the trailing-tag case below). * * Right-side breathing room: the trailing chip is the last element * before the row-× button. Without margin-right it crowds the remove * button on narrow viewports. 0.5em gives comfortable separation. * * Internal padding-right: the chip's input has `padding: 0` and uses * `field-sizing: content` so it hugs the placeholder text exactly. * Without internal padding-right the chip ends abruptly right after * the "add" text. 0.25em (4pt) adds a small interior trailing space * so the chip's bounds read as "+ add ⎵" rather than "+ add|". This * is internal breathing room; the margin-right above is external * separation from the row-× button. */ .cm-frontmatter-prop-chip-trailing { margin-right: 0.5em; padding-right: 0.25em; } .cm-frontmatter-prop-chip-trailing .cm-frontmatter-prop-chip-input::placeholder { color: var(--text-tertiary); font-style: italic; opacity: 1; } .cm-frontmatter-prop-chip-trailing::before { content: '+'; display: inline-block; margin-right: 0.15em; color: var(--text-tertiary); opacity: 0.5; font-weight: 600; user-select: none; } /* The × is always visible (no hover reveal). The earlier hover-only * affordance was confusing on touch devices and added visual jitter * when the cursor moved between chips. Steady-state visibility, with * an opacity bump on hover to signal interactivity. * * Default opacity 0.5 (#277): chip-× is dimmed by default to give the * row-× clear primacy. On a tags row with N chips the user sees N * dimmed chip-×s and ONE crisp row-× — the visual hierarchy reads * "remove a tag" (incidental) vs "remove the property" (deliberate) * even at a glance. Hover lifts the chip-× to full strength. */ .cm-frontmatter-prop-chip-remove, .cm-frontmatter-prop-chip-remove-placeholder { display: inline-flex; align-items: center; justify-content: center; width: 1rem; height: 1rem; padding: 0; margin: 0; border: none; border-radius: 2px; background-color: transparent; color: var(--text-tertiary); opacity: 0.5; font: inherit; font-size: 1em; line-height: 1; cursor: pointer; /* Match the opacity-transition idiom used by sibling chips * (.cm-wikilink-chip et al.) — without this the dim→bright on hover * is abrupt (#D6 adversary fix). */ transition: opacity 0.1s ease; } .cm-frontmatter-prop-chip-remove:hover, .cm-frontmatter-prop-chip-remove:focus-visible { color: var(--text-primary); opacity: 1; } .cm-frontmatter-prop-chip > .cm-frontmatter-prop-chip-remove { margin-left: -0.05em; } /* #282 round-3: chip-× lives inside .cm-frontmatter-prop-row which has * `overflow: hidden` (added in this fix). The default UA focus outline * would be clipped on top/bottom edges — same a11y bug round-2 fixed * for the sibling row-× and add-cancel buttons. Apply the same * inset-ring + tinted-background treatment here. The combined :hover, * :focus-visible rule above shares color+opacity changes between the * two states; this rule layers focus-only chrome on top so a keyboard * user can distinguish "focused" from "merely hovered." * * Source-order placement: this rule comes AFTER the combined * :hover, :focus-visible block so :focus-visible's box-shadow + * background paint over (not under) the shared color/opacity changes. * Specificity is identical (single pseudo-class), so order wins. */ .cm-frontmatter-prop-chip-remove:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); background-color: color-mix(in srgb, var(--accent) 12%, transparent); } /* Frontmatter tags opt into the canonical `.reams-tag-chip` styling from * src/components/tag-chip.css. This local rule only preserves the widget's * editable flex layout. */ .cm-frontmatter-tag-chip { display: inline-flex; gap: 0.125rem; } /* Tag chips use a smaller font than the surrounding field controls. Keep * their control box on the shared height token so the tag and property remove * buttons have the same first-line center on every viewport. */ .cm-root-frontmatter-surface .cm-frontmatter-tag-chip { height: var(--reams-frontmatter-control-height); min-height: var(--reams-frontmatter-control-height); box-sizing: border-box; } /* Compatibility alias for tests and historical selectors. The canonical * prefix definition lives in `.reams-tag-chip-prefix::before`. */ .cm-frontmatter-tag-chip::before { margin-right: 0.15em; } /* When the tag chip is the trailing empty/placeholder slot, swap the * `#` prefix for a `+` glyph (#280): the slot is for ADDING a tag, * not yet a tag itself. Same color/opacity as the plain trailing * chip's `+` so the affordance reads identically across list and * tags variants. Specificity-wise this two-class selector overrides * the single-class `.cm-frontmatter-prop-chip-trailing::before` * defined below. */ .cm-frontmatter-tag-chip.cm-frontmatter-prop-chip-trailing::before { content: '+'; color: var(--text-tertiary); opacity: 0.5; font-weight: 600; } .cm-frontmatter-tag-chip.cm-frontmatter-prop-chip-readonly-spacer::before { content: '+'; color: var(--text-tertiary); opacity: 0.5; font-weight: 600; } @media (max-width: 767px) { /* Mobile: tag chips can dominate the single-row scroll lane, so trim the * chrome slightly without changing the overall one-line interaction model. */ .cm-frontmatter-tag-chip { display: inline-flex; } } .cm-frontmatter-tag-chip .cm-frontmatter-prop-chip-remove, .cm-frontmatter-tag-chip .cm-frontmatter-prop-chip-remove-placeholder, .reams-root-type-chip > .cm-frontmatter-prop-chip-remove { width: 0.75rem; height: 1rem; margin-left: 0; color: var(--reams-tag-chip-color, var(--accent)); } .reams-root-type-chip > .cm-frontmatter-prop-chip-remove { color: var(--reams-type-chip-text); } .cm-frontmatter-tag-chip.reams-tag-chip { padding-left: var(--reams-tag-chip-padding-inline); padding-right: var(--reams-tag-chip-remove-padding-inline, 2px); color: var(--reams-tag-chip-color); } @media (pointer: coarse) { .cm-frontmatter-tag-chip .cm-frontmatter-prop-chip-remove, .reams-root-type-chip > .cm-frontmatter-prop-chip-remove { position: relative; min-width: 0.75rem; min-height: 1em; } .cm-frontmatter-tag-chip .cm-frontmatter-prop-chip-remove::after, .reams-root-type-chip > .cm-frontmatter-prop-chip-remove::after { content: ''; position: absolute; width: 24px; height: 24px; left: 50%; top: 50%; transform: translate(-50%, -50%); } } .cm-frontmatter-tag-chip .cm-frontmatter-prop-chip-remove:hover { color: var(--text-primary); } .reams-root-type-chip > .cm-frontmatter-prop-chip-remove:is(:hover, :focus-visible) { color: var(--reams-type-chip-text); opacity: 1; } .reams-root-type-chip > .cm-frontmatter-prop-chip-remove:focus-visible { box-shadow: inset 0 0 0 2px color-mix(in oklch, var(--reams-type-color, var(--accent)) 60%, var(--text-primary)); background-color: color-mix(in oklch, var(--reams-type-color, var(--accent)) 18%, transparent); } /* Per-row remove (× on the right edge of each property row). * Always visible — the earlier hover-reveal added jitter and didn't * work on touch devices. * * Visual hierarchy vs chip-× (#277): the row-× nukes the entire * property; the chip-× removes a single list item. Two dimensions of * differentiation: * 1. Position — `margin-left: auto` pushes the row-× to a fixed * right gutter so it never sits adjacent to chip-×s on the same * visual line. Even when a tags row's chips wrap, the row-× sits * in its own column at the row's right edge. * 2. Intensity — chip-×s are dimmed to opacity 0.5 (see below); the * row-× stays at full text-tertiary intensity. Combined with the * gutter this gives the row-× clear primacy even though the * chip-× shares the same color token. * * `align-self: flex-start` anchors the button to the row's first line * rather than centering against a wrapped value cell (#278). With the * row-level `align-items: flex-start` (D7 round-3) this is redundant * for the default case, but kept explicit so any future change to the * row's alignment can't silently re-introduce row-× drift on wrap. */ .cm-frontmatter-prop-remove, .cm-frontmatter-prop-remove-placeholder { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; align-self: flex-start; margin-left: auto; width: 1.4em; height: var(--reams-frontmatter-control-height, 1.4em); padding: 0; border: none; border-radius: 2px; background-color: transparent; color: var(--text-tertiary); font: inherit; font-size: 1em; line-height: 1; cursor: pointer; } .cm-root-frontmatter-surface .cm-frontmatter-prop-row:has(> .cm-frontmatter-prop-value > .reams-yaml-object-summary) > .cm-frontmatter-prop-remove { height: 1.4em; } .cm-frontmatter-prop-remove:hover { color: var(--text-primary); } /* #282: explicit :focus-visible with an INSET focus indicator (box-shadow * + color, not outline). The row's `overflow: hidden` (added in the same * fix) would clip a default UA outline on top/bottom edges, breaking * keyboard a11y on the row-× button. Use box-shadow inset which paints * inside the element's own box and survives ancestor clipping. * * 2px ring (not 1px) because the button is small (1.4em ≈ 22px on * default 16px root font) and a single-pixel ring against the row's * background is marginal at WCAG 2.4.7 threshold. The translucent * accent-tinted background reinforces focus distinctly from :hover * (which is color-only). */ .cm-frontmatter-prop-remove:focus-visible { outline: none; color: var(--text-primary); box-shadow: inset 0 0 0 2px var(--accent); background-color: color-mix(in srgb, var(--accent) 12%, transparent); } @media (pointer: coarse) { .cm-frontmatter-prop-chip-remove { position: relative; min-width: 1rem; min-height: 1rem; } .cm-frontmatter-prop-chip-remove::after { content: ''; position: absolute; width: 24px; height: 24px; left: 50%; top: 50%; transform: translate(-50%, -50%); } .cm-frontmatter-prop-remove { min-width: 24px; min-height: 24px; } } /* Add-property row at the bottom. Chromeless: no top border, no * dedicated background. Two states: * - Idle: `+ Add property` button only. * - Active: name input | value input | × cancel button. */ .cm-frontmatter-prop-add { display: flex; align-items: center; gap: 0.5em; padding: 0.1em 0; } .cm-frontmatter-prop-add-btn, .cm-frontmatter-prop-add-placeholder { background-color: transparent; color: var(--text-tertiary); border: none; font: inherit; cursor: pointer; padding: 0; } .cm-frontmatter-prop-add-btn:hover, .cm-frontmatter-prop-add-btn:focus { color: var(--accent); outline: none; } /* Active-mode name field — same width as a regular property key, * mirroring the mobile-first 4em / desktop 8em sizing pattern below. * Borderless by default like other key inputs; gains the focus ring * on focus. */ .cm-frontmatter-prop-add-name { flex: 0 0 4em; width: 4em; padding: 0 0.25em; background-color: transparent; color: var(--text-primary); border: 1px solid transparent; border-radius: 2px; font: inherit; } .cm-frontmatter-prop-add-name:focus { outline: none; border-color: var(--accent); } /* Active-mode value field — fills remaining row width. */ .cm-frontmatter-prop-add-value { flex: 1 1 auto; min-width: 4em; padding: 0 0.35em; background-color: transparent; color: var(--text-primary); border: 1px solid transparent; border-radius: 2px; font: inherit; } .cm-frontmatter-prop-add-value:hover { border-color: var(--text-tertiary); } .cm-frontmatter-prop-add-value:focus { outline: none; border-color: var(--accent); } .cm-frontmatter-prop-add-cancel { flex: 0 0 auto; width: 1.4em; height: 1.4em; padding: 0; border: none; border-radius: 2px; background-color: transparent; color: var(--text-tertiary); font: inherit; font-size: 1em; line-height: 1; cursor: pointer; } .cm-frontmatter-prop-add-cancel:hover { color: var(--text-primary); } /* #282: inset focus indicator for the same reason as .cm-frontmatter-prop-remove. * The add-property row sits inside `.cm-frontmatter-properties-table` which * has `overflow: hidden`; an outline-style focus ring would clip. Same * 2px-ring + tinted-background treatment for a11y parity. */ .cm-frontmatter-prop-add-cancel:focus-visible { outline: none; color: var(--text-primary); box-shadow: inset 0 0 0 2px var(--accent); background-color: color-mix(in srgb, var(--accent) 12%, transparent); } /* Read-mode treatment. The widget JS sets `disabled` on every input; * the additional class lets us tone down the entire wrapper. */ .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-input:disabled, .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-key:disabled, .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-chip-input:disabled, .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-checkbox:disabled { cursor: default; opacity: 0.85; } /* Read mode drops the interactive affordance paint, so reserve the same right * gutter the edit-mode controls occupy to keep table bounds stable. */ .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-value { padding-right: 8px; box-sizing: border-box; } .cm-frontmatter-prop-remove-placeholder, .cm-frontmatter-prop-chip-remove-placeholder, .cm-frontmatter-prop-chip-readonly-spacer, .cm-frontmatter-prop-add-placeholder { visibility: hidden; pointer-events: none; } .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-input:hover:disabled, .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-key:hover:disabled { border-color: transparent; } /* CURSOR-SKIP JUSTIFICATION (#269): widget chrome inside the * frontmatter properties widget — these are the per-row "remove" and * "add" buttons that allow editing in live-preview mode. The * `cm-frontmatter-properties-table-readonly` wrapper is set when the * editor is in read mode, where the widget itself is rendered * read-only and the editing affordances are hidden via `display: none`. * None of these elements are caret-targetable text in any mode (they * are buttons inside a CM6 widget that's rendered with * contenteditable=false), so `display: none` cannot cause cursor- * skip — the widget chrome is outside the editor's caret flow. */ .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-remove, .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-chip-remove, .cm-frontmatter-properties-table-readonly .cm-frontmatter-prop-chip-trailing { display: none; } /* Frontmatter widget: desktop column widening. Mobile-first defaults * give the key column a tight 4em (~64px on default font size) so * the value cell isn't crowded on a ~360px phone viewport. At * tablet/desktop widths there's plenty of room for a wider key * column that improves at-a-glance scannability across rows. * * Breakpoint: 768px matches the existing mobile/desktop split used * by the rest of cm6-editor.css (see image-viewer + toolbar * clearance rules below). */ @media (min-width: 768px) { .cm-frontmatter-prop-key, .cm-frontmatter-prop-key-readonly, .cm-frontmatter-prop-add-name { flex-basis: 8em; width: 8em; } } /* ---- Image viewer lightbox overlay (#231) ---- * * Fullscreen overlay for viewing image embeds at natural resolution. * Triggered by clicking any rendered image embed or gallery image. * Vanilla-DOM component (ImageViewerOverlay class), appended to * document.body — NOT inside the CM6 editor container. * * z-index: 2100 — above GestureIndicator (2000), settings modal (1000), * and fuzzy search overlay (1000). The lightbox should always be the * topmost layer since it's a transient fullscreen view. * -------------------------------------------------------------------------- */ /* The overlay root doubles as the backdrop — its dark background is the * click-to-dismiss target. No separate backdrop div is needed; a sibling * backdrop would be unreachable behind the full-viewport content wrapper * in the stacking context. */ .image-viewer-overlay { position: fixed; inset: 0; z-index: 2100; display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr; background: rgba(0, 0, 0, 0.9); } /* Content wrapper is pointer-transparent so clicks on the backdrop area * (the overlay root behind the content) reach the overlay's click-to-dismiss * handler. Interactive children restore pointer-events: auto individually. */ .image-viewer-content { /* NOTE: position: relative removed intentionally. The close button uses * absolute positioning, but it now falls back to the fixed-positioned * .image-viewer-overlay ancestor as its containing block, which works * correctly since the overlay covers the viewport. */ grid-row: 1; display: flex; align-items: center; justify-content: center; min-height: 60vh; /* Spec: image must retain at least 60% of viewport */ padding: 2rem; box-sizing: border-box; pointer-events: none; } .image-viewer-img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px; user-select: none; -webkit-user-select: none; pointer-events: auto; } /* Close button — top-right corner. * * CURSOR-SKIP JUSTIFICATION (#269): overlay chrome — this button is a * `