amdhj22_rbr.obsidian/theme.css
Amdhj22 d9161cb81f Auto hide: kill typing-time recalc, add tunable hit zones
Performance fixes (verified with DevTools Performance traces):

- Drop body-level :has() selectors. They were causing 3,500+ element
  style recalcs on every CodeMirror keystroke. Synchronized expand
  is restored via .titlebar:hover ~ * (general-sibling combinator),
  which doesn't trigger the same invalidation cost.
- @property --ribbon-width: inherits: true -> false. The typed
  inherited property was pulling the entire editor subtree into the
  cascade on every recalc. Variable is now declared on the ribbon
  element directly, not body.
- Remove overflow: visible from .workspace-split.mod-root. It was
  expanding the editor's paint damage rect on every keystroke. Side
  panel splits keep overflow: visible since they don't contain the
  editor — that gives sidebar strips lateral hit zone room.

Hit zones (tunable via Style Settings):

- Wide top-of-window band anchored on the titlebar pseudo (escapes
  workspace-split clipping; same slider as per-strip zones, up to
  100px). Hovering it expands all three strips at once.
- Each strip gets ::before above + ::after below within its own
  panel — bounded to panel width (no -100vw extension) to prevent
  cross-panel hover bleed.
- Ribbon gets a ::before overlay extending its hit zone width to
  the right, with its own slider (up to 100px).

New Style Settings sliders:

- Expand/collapse animation duration (60–280ms)
- Tab strip hover hit zone height (4–100px)
- Left ribbon hover hit zone width (6–100px)

Trade: pseudo bands capture clicks within their area. Defaults are
small enough to be invisible in practice; dial accordingly when
turning the sliders up.
2026-05-29 00:41:24 +09:00

1548 lines
59 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* RBR — Red Bull Racing colorscheme for Obsidian
*
* Three-accent palette:
* - Kerb Red (#e84a55) — the active/selected thing (default accent)
* - RB Yellow (#ffd84d) — attention / cursor / modifications
* - Chequer White (#ffffff) — functions / methods (third dominant accent)
*
* Background is low-saturation navy so the trio dominates without harshness.
* For the design rules behind every color choice, see the upstream
* STYLE-GUIDE.md at https://github.com/Amdhj22/rbr
*
* RBR currently ships only the "classic" (dark) flavor. The theme applies
* the same dark palette under both .theme-dark and .theme-light so users
* with the global light mode still get a coherent experience.
*
* --- User options (Style Settings plugin) ---
* Install the "Style Settings" community plugin to expose:
* - Primary accent color (red / yellow / white)
* - Heading style (trio / mono / accent-only)
* - Cursor color (yellow / warm / follow accent)
* - Active tab indicator (top / bottom / fill)
* - Code font size slider
* - Highlight (==text==) intensity slider
* - Auto hide toggles (tab bar / status bar / ribbon / vault profile)
* - Card layout toggle + corner-radius / gap sliders
* Without the plugin, defaults apply and nothing breaks.
*/
/* @settings
name: RBR
id: rbr-theme
settings:
-
id: rbr-section-accent
title: Accent
type: heading
level: 2
collapsed: false
-
id: rbr-accent-choice
title: Primary accent color
description: |
활성/선택 항목 강조에 쓰일 메인 색.
RBR 기본 룰은 Kerb Red. 커서는 별개 — 아래 Cursor 옵션 참고.
type: class-select
default: rbr-accent-red
allowEmpty: false
options:
-
label: Kerb Red (기본)
value: rbr-accent-red
-
label: RB Yellow
value: rbr-accent-yellow
-
label: Chequer White
value: rbr-accent-white
-
id: rbr-section-headings
title: Headings
type: heading
level: 2
collapsed: false
-
id: rbr-headings-choice
title: Heading style
description: |
h1~h3 색상 패턴.
Trio : h1=accent · h2=yellow · h3=white (RBR 기본)
Mono : 모든 헤딩이 본문 색 (강조 최소화)
Accent only : h1만 accent, 나머지는 본문 색
type: class-select
default: rbr-headings-trio
allowEmpty: false
options:
-
label: Trio (기본)
value: rbr-headings-trio
-
label: Monochrome
value: rbr-headings-mono
-
label: Accent only
value: rbr-headings-accent-only
-
id: rbr-section-cursor
title: Cursor
type: heading
level: 2
collapsed: false
-
id: rbr-cursor-choice
title: Cursor color
description: |
에디터 커서 색.
RB Yellow : RBR 기본 — 항상 주목 픽셀
RB Warm : 한 단계 덜 자극적인 따뜻한 노랑
Follow accent : 위 Primary accent를 따라감
type: class-select
default: rbr-cursor-yellow
allowEmpty: false
options:
-
label: RB Yellow (기본)
value: rbr-cursor-yellow
-
label: RB Warm
value: rbr-cursor-warm
-
label: Follow accent
value: rbr-cursor-accent
-
id: rbr-section-tab
title: Tab indicator
type: heading
level: 2
collapsed: false
-
id: rbr-tab-choice
title: Active tab indicator
description: |
활성 탭 강조 방식.
Top : 위쪽에 accent 색 띠 (RBR 기본)
Bottom : 아래쪽 띠
Fill : 탭 전체 배경에 accent 옅게 깔기
type: class-select
default: rbr-tab-top
allowEmpty: false
options:
-
label: Top border (기본)
value: rbr-tab-top
-
label: Bottom border
value: rbr-tab-bottom
-
label: Background fill
value: rbr-tab-fill
-
id: rbr-section-typography
title: Typography
type: heading
level: 2
collapsed: false
-
id: rbr-code-scale
title: Code font size
description: 코드 블록과 인라인 코드의 폰트 크기 (em).
type: variable-number-slider
default: 0.92
min: 0.80
max: 1.10
step: 0.01
format: em
-
id: rbr-highlight-alpha
title: Highlight intensity
description: |
==하이라이트== 배경 alpha. 검색 매치도 같은 값을 사용.
0.10 = 거의 안 보임, 0.60 = 강한 강조.
type: variable-number-slider
default: 0.30
min: 0.10
max: 0.60
step: 0.05
-
id: rbr-section-autohide
title: Auto hide
description: |
마우스를 올렸을 때만 보이도록 UI 요소를 숨긴다.
호버가 없는 모바일에서는 자동 비활성.
type: heading
level: 2
collapsed: true
-
id: rbr-autohide-tabs
title: Auto hide tab bar
description: 탭 헤더 바를 숨기고 상단/타이틀바에 호버하면 펼침.
type: class-toggle
-
id: rbr-autohide-statusbar
title: Auto hide status bar
description: 하단 상태바를 가는 띠로 줄이고 호버 시 펼침.
type: class-toggle
-
id: rbr-autohide-ribbon
title: Auto hide left ribbon
description: 좌측 리본 메뉴를 가는 띠로 줄이고 호버 시 펼침.
type: class-toggle
-
id: rbr-autohide-vault-profile
title: Auto hide vault profile
description: 좌측 사이드바 하단의 vault 프로필 영역 숨김. 사이드바 호버 시 펼침.
type: class-toggle
-
id: rbr-autohide-delay
title: Hover delay before expanding
description: |
호버 후 펼쳐지기까지 대기 시간 (ms).
커서가 잠깐 지나가는 것만으로 깜빡이지 않게 하는 hover-intent 지연.
0 = 즉시 펼침, 200 = 짧은 의도 확인, 400+ = 의도적으로 머무를 때만.
type: variable-number-slider
default: 200
min: 0
max: 600
step: 50
format: ms
-
id: rbr-autohide-duration
title: Expand/collapse animation duration
description: |
자동 숨김 영역이 펼쳐지고 닫히는 애니메이션 길이 (ms).
짧을수록 가볍고 스냅함. 80120ms 가 perf/feel 균형점.
layout-property transition 비용을 줄여 마우스 hover 시
메인 스레드 부담을 직접 낮춤.
type: variable-number-slider
default: 100
min: 60
max: 280
step: 20
format: ms
-
id: rbr-autohide-hitzone
title: Tab strip hover hit zone height
description: |
접힌 탭 strip 위로 보이지 않는 hover 영역의 높이 (px).
메인 에디터/좌우 사이드 패널 strip 모두에 적용.
클수록 마우스로 노리기 쉽지만 너무 크면 의도치 않게 펼쳐짐.
type: variable-number-slider
default: 12
min: 4
max: 100
step: 4
format: px
-
id: rbr-autohide-ribbon-hitzone
title: Left ribbon hover hit zone width
description: |
접힌 좌측 ribbon 옆으로 보이지 않는 hover 영역의 폭 (px).
ribbon 자체는 6px 그대로 두고, 오른쪽으로 가상의 호버 영역만 확장.
이 영역으로 마우스가 들어오면 ribbon 이 펼쳐짐.
주의: 영역 안에서 클릭은 ribbon 으로 캡처됨 (editor 영역과 겹치므로).
type: variable-number-slider
default: 20
min: 6
max: 100
step: 4
format: px
-
id: rbr-section-card
title: Card layout
description: |
워크스페이스 패널을 카드처럼 분리해서 표시.
여백 + 둥근 모서리 + 부드러운 그림자가 추가된다.
type: heading
level: 2
collapsed: true
-
id: rbr-card-layout
title: Enable card layout
description: 카드 레이아웃 켜기/끄기.
type: class-toggle
-
id: rbr-card-radius
title: Card corner radius
description: 카드 모서리 라운딩 (px).
type: variable-number-slider
default: 8
min: 0
max: 20
step: 1
format: px
-
id: rbr-card-gap
title: Card gap
description: 카드 사이 간격 (px).
type: variable-number-slider
default: 12
min: 0
max: 24
step: 1
format: px
*/
/* ========================================================================
Palette tokens (single source of truth)
======================================================================== */
.theme-dark,
.theme-light {
/* ---- Neutral layers (lightness ladder) ---- */
--rbr-crust: #05091a; /* ANSI black, gutter shadow */
--rbr-mantle: #070d1f; /* sidebar, secondary panes */
--rbr-base: #0a1128; /* primary background */
--rbr-surface-0: #121a36; /* floating panels, tooltips */
--rbr-surface-1: #15203f; /* current-line, hover */
--rbr-surface-2: #1f2a52; /* selection, active tab fill */
--rbr-overlay-0: #3a4466; /* line numbers, inactive borders */
--rbr-overlay-1: #4a5580; /* medium borders */
--rbr-overlay-2: #565f80; /* strong borders */
--rbr-subtext-0: #6a7495; /* comments, placeholders */
--rbr-subtext-1: #8590ae; /* secondary text */
--rbr-text: #c8d0e8; /* primary text */
--rbr-track-line: #e8ecf5; /* hottest white */
/* ---- Brand trio + accent extras ---- */
--rbr-kerb-red: #e84a55;
--rbr-kerb-red-bright: #f56570;
--rbr-kerb-red-pure: #cc1e4a;
--rbr-rb-yellow: #ffd84d;
--rbr-rb-warm: #f5c842;
--rbr-rb-pure: #ffc906;
--rbr-chequer-white: #ffffff;
--rbr-oracle-blue: #82a0d8;
--rbr-sky-blue: #a4bde8;
--rbr-track-green: #9cc080;
--rbr-paddock-green: #b8d49e;
--rbr-cool-grey: #a8a8b0;
--rbr-light-grey: #c8c8d0;
--rbr-livery-blue: #2563eb;
--rbr-livery-blue-bright:#4D7EFF;
/* RGB triplets for places Obsidian expects --…-rgb tokens */
--rbr-base-rgb: 10, 17, 40;
--rbr-kerb-red-rgb: 232, 74, 85;
--rbr-kerb-bright-rgb: 245, 101, 112;
--rbr-rb-yellow-rgb: 255, 216, 77;
--rbr-rb-warm-rgb: 245, 200, 66;
--rbr-paddock-green-rgb: 184, 212, 158;
--rbr-oracle-blue-rgb: 130, 160, 216;
--rbr-chequer-white-rgb: 255, 255, 255;
/* ---- Indirect "active accent" slot — Style Settings retargets this ----
Defaults to kerb red; body.rbr-accent-{yellow,white} below override. */
--rbr-accent: var(--rbr-kerb-red);
--rbr-accent-rgb: var(--rbr-kerb-red-rgb);
--rbr-accent-hover: var(--rbr-kerb-red-bright);
/* ---- Indirect "cursor" slot — defaults to RB Yellow.
The -rgb variant is also exposed so selection backgrounds
(incl. vim visual mode) can alpha-blend the same color. */
--rbr-cursor: var(--rbr-rb-yellow);
--rbr-cursor-rgb: var(--rbr-rb-yellow-rgb);
}
/* Style Settings injects body.<value-of-class-select> on <body>. */
/* Accent overrides */
body.rbr-accent-red {
--rbr-accent: var(--rbr-kerb-red);
--rbr-accent-rgb: var(--rbr-kerb-red-rgb);
--rbr-accent-hover: var(--rbr-kerb-red-bright);
}
body.rbr-accent-yellow {
--rbr-accent: var(--rbr-rb-yellow);
--rbr-accent-rgb: var(--rbr-rb-yellow-rgb);
--rbr-accent-hover: var(--rbr-rb-pure);
}
body.rbr-accent-white {
--rbr-accent: var(--rbr-chequer-white);
--rbr-accent-rgb: var(--rbr-chequer-white-rgb);
--rbr-accent-hover: var(--rbr-track-line);
}
/* Cursor overrides — variable level (color + rgb triplet for selection alpha) */
body.rbr-cursor-yellow {
--rbr-cursor: var(--rbr-rb-yellow);
--rbr-cursor-rgb: var(--rbr-rb-yellow-rgb);
}
body.rbr-cursor-warm {
--rbr-cursor: var(--rbr-rb-warm);
--rbr-cursor-rgb: var(--rbr-rb-warm-rgb);
}
body.rbr-cursor-accent {
--rbr-cursor: var(--rbr-accent);
--rbr-cursor-rgb: var(--rbr-accent-rgb);
}
/* Cursor overrides — direct selector enforcement.
CodeMirror 6 paints the caret as a border on .cm-cursor, which doesn't
always pick up --caret-color via cascade. Force both caret-color and
border-left-color so the cursor changes regardless. */
body.rbr-cursor-yellow .cm-content,
body.rbr-cursor-yellow .cm-cursor,
body.rbr-cursor-yellow .cm-cursor.cm-cursor-primary,
body.rbr-cursor-yellow .CodeMirror-cursor {
caret-color: var(--rbr-rb-yellow) !important;
border-left-color: var(--rbr-rb-yellow) !important;
}
body.rbr-cursor-warm .cm-content,
body.rbr-cursor-warm .cm-cursor,
body.rbr-cursor-warm .cm-cursor.cm-cursor-primary,
body.rbr-cursor-warm .CodeMirror-cursor {
caret-color: var(--rbr-rb-warm) !important;
border-left-color: var(--rbr-rb-warm) !important;
}
body.rbr-cursor-accent .cm-content,
body.rbr-cursor-accent .cm-cursor,
body.rbr-cursor-accent .cm-cursor.cm-cursor-primary,
body.rbr-cursor-accent .CodeMirror-cursor {
caret-color: var(--rbr-accent) !important;
border-left-color: var(--rbr-accent) !important;
}
/* Vim mode block cursor — normal/visual modes draw .cm-fat-cursor as a
filled block, not a thin caret. Different paint property (background +
outline), so we have to target it separately.
.cm-fat-cursor — focused primary cursor (filled block)
.cm-fat-cursor:not(.cm-cursor-primary)
— unfocused / secondary (outlined only)
The --rbr-cursor variable already follows the user's choice
(yellow / warm / accent) so we just plug it in. */
.cm-fat-cursor,
.cm-fat-cursor.cm-cursor-primary {
background-color: var(--rbr-cursor) !important;
outline-color: var(--rbr-cursor) !important;
border-color: var(--rbr-cursor) !important;
/* keep the character readable underneath the block fill */
color: var(--rbr-base) !important;
}
.cm-fat-cursor:not(.cm-cursor-primary) {
background-color: transparent !important;
outline: 1px solid var(--rbr-cursor) !important;
color: inherit !important;
}
/* Some Obsidian builds toggle .cm-fat-cursor on the editor host instead of
the cursor element — cover both cases */
.cm-editor.cm-fat-cursor .cm-cursor,
.cm-editor.cm-fat-cursor .cm-cursor.cm-cursor-primary {
background-color: var(--rbr-cursor) !important;
outline-color: var(--rbr-cursor) !important;
border-color: var(--rbr-cursor) !important;
}
/* ========================================================================
Core Obsidian variables
======================================================================== */
.theme-dark,
.theme-light {
/* ---- Backgrounds ---- */
--background-primary: var(--rbr-base);
--background-primary-alt: var(--rbr-mantle);
--background-secondary: var(--rbr-mantle);
--background-secondary-alt: var(--rbr-crust);
--background-modifier-border: var(--rbr-surface-1);
--background-modifier-border-hover: var(--rbr-overlay-0);
--background-modifier-border-focus: var(--rbr-accent);
--background-modifier-hover: var(--rbr-surface-1);
--background-modifier-active-hover: var(--rbr-surface-2);
--background-modifier-form-field: var(--rbr-surface-0);
--background-modifier-form-field-highlighted: var(--rbr-surface-1);
--background-modifier-box-shadow: rgba(5, 9, 26, 0.5);
--background-modifier-error: rgba(var(--rbr-kerb-bright-rgb), 0.20);
--background-modifier-error-rgb: var(--rbr-kerb-bright-rgb);
--background-modifier-error-hover: rgba(var(--rbr-kerb-bright-rgb), 0.30);
--background-modifier-success: rgba(var(--rbr-paddock-green-rgb), 0.20);
--background-modifier-message: rgba(10, 17, 40, 0.9);
/* ---- Text ---- */
--text-normal: var(--rbr-text);
--text-muted: var(--rbr-subtext-1);
--text-faint: var(--rbr-subtext-0);
--text-on-accent: var(--rbr-base);
--text-error: var(--rbr-kerb-red-bright);
--text-warning: var(--rbr-rb-yellow);
--text-success: var(--rbr-paddock-green);
--text-accent: var(--rbr-accent);
--text-accent-hover: var(--rbr-accent-hover);
--text-selection: rgba(var(--rbr-accent-rgb), 0.25);
/* Highlight intensity — driven by --rbr-highlight-alpha slider (default 0.30) */
--text-highlight-bg: rgba(var(--rbr-rb-yellow-rgb), var(--rbr-highlight-alpha, 0.30));
--text-highlight-bg-active: rgba(var(--rbr-rb-yellow-rgb), calc(var(--rbr-highlight-alpha, 0.30) + 0.15));
/* ---- Interactive ---- */
--interactive-normal: var(--rbr-surface-0);
--interactive-hover: var(--rbr-surface-1);
--interactive-accent: var(--rbr-accent);
--interactive-accent-hover: var(--rbr-accent-hover);
--interactive-accent-rgb: var(--rbr-accent-rgb);
--interactive-success: var(--rbr-paddock-green);
/* ---- Caret — driven by --rbr-cursor (default RB Yellow) ---- */
--caret-color: var(--rbr-cursor);
/* ---- Borders / focus ---- */
--divider-color: var(--rbr-surface-1);
--divider-color-hover: var(--rbr-overlay-0);
/* ---- Indentation guides ---- */
--indentation-guide: var(--rbr-overlay-0);
--indentation-guide-active: var(--rbr-accent);
}
/* ========================================================================
Headings — Style Settings (rbr-headings-*) controls the pattern.
Default trio: h1=accent · h2=yellow · h3=white.
======================================================================== */
.theme-dark,
.theme-light {
--h1-color: var(--rbr-accent);
--h2-color: var(--rbr-rb-yellow);
--h3-color: var(--rbr-chequer-white);
--h4-color: var(--rbr-paddock-green);
--h5-color: var(--rbr-oracle-blue);
--h6-color: var(--rbr-sky-blue);
--h1-weight: 800;
--h2-weight: 700;
--h3-weight: 700;
--h4-weight: 600;
--h5-weight: 600;
--h6-weight: 600;
}
/* Monochrome — every heading uses normal text color */
body.rbr-headings-mono {
--h1-color: var(--rbr-text);
--h2-color: var(--rbr-text);
--h3-color: var(--rbr-text);
--h4-color: var(--rbr-text);
--h5-color: var(--rbr-text);
--h6-color: var(--rbr-text);
}
/* Accent only — h1 keeps the accent, everything else is normal text */
body.rbr-headings-accent-only {
--h1-color: var(--rbr-accent);
--h2-color: var(--rbr-text);
--h3-color: var(--rbr-text);
--h4-color: var(--rbr-text);
--h5-color: var(--rbr-text);
--h6-color: var(--rbr-text);
}
/* Direct selector enforcement — covers reading view, live preview (CM6),
and source mode. Obsidian/CodeMirror occasionally resolve heading color
without going through --h1-color, so we paint the elements directly too. */
/* Reading view + rendered markdown */
body.rbr-headings-mono .markdown-rendered h1,
body.rbr-headings-mono .markdown-rendered h2,
body.rbr-headings-mono .markdown-rendered h3,
body.rbr-headings-mono .markdown-rendered h4,
body.rbr-headings-mono .markdown-rendered h5,
body.rbr-headings-mono .markdown-rendered h6,
body.rbr-headings-mono .markdown-preview-view h1,
body.rbr-headings-mono .markdown-preview-view h2,
body.rbr-headings-mono .markdown-preview-view h3,
body.rbr-headings-mono .markdown-preview-view h4,
body.rbr-headings-mono .markdown-preview-view h5,
body.rbr-headings-mono .markdown-preview-view h6,
/* Live preview (CM6 HyperMD line classes) */
body.rbr-headings-mono .HyperMD-header,
body.rbr-headings-mono .HyperMD-header-1,
body.rbr-headings-mono .HyperMD-header-2,
body.rbr-headings-mono .HyperMD-header-3,
body.rbr-headings-mono .HyperMD-header-4,
body.rbr-headings-mono .HyperMD-header-5,
body.rbr-headings-mono .HyperMD-header-6,
/* Source mode token classes */
body.rbr-headings-mono .cm-header,
body.rbr-headings-mono .cm-header-1,
body.rbr-headings-mono .cm-header-2,
body.rbr-headings-mono .cm-header-3,
body.rbr-headings-mono .cm-header-4,
body.rbr-headings-mono .cm-header-5,
body.rbr-headings-mono .cm-header-6,
/* Inline title in the editor header */
body.rbr-headings-mono .inline-title {
color: var(--rbr-text) !important;
}
/* Accent-only — h2..h6 + their CM equivalents fall back to text */
body.rbr-headings-accent-only .markdown-rendered h2,
body.rbr-headings-accent-only .markdown-rendered h3,
body.rbr-headings-accent-only .markdown-rendered h4,
body.rbr-headings-accent-only .markdown-rendered h5,
body.rbr-headings-accent-only .markdown-rendered h6,
body.rbr-headings-accent-only .markdown-preview-view h2,
body.rbr-headings-accent-only .markdown-preview-view h3,
body.rbr-headings-accent-only .markdown-preview-view h4,
body.rbr-headings-accent-only .markdown-preview-view h5,
body.rbr-headings-accent-only .markdown-preview-view h6,
body.rbr-headings-accent-only .HyperMD-header-2,
body.rbr-headings-accent-only .HyperMD-header-3,
body.rbr-headings-accent-only .HyperMD-header-4,
body.rbr-headings-accent-only .HyperMD-header-5,
body.rbr-headings-accent-only .HyperMD-header-6,
body.rbr-headings-accent-only .cm-header-2,
body.rbr-headings-accent-only .cm-header-3,
body.rbr-headings-accent-only .cm-header-4,
body.rbr-headings-accent-only .cm-header-5,
body.rbr-headings-accent-only .cm-header-6 {
color: var(--rbr-text) !important;
}
/* Accent-only — h1 + inline title keep the accent */
body.rbr-headings-accent-only .markdown-rendered h1,
body.rbr-headings-accent-only .markdown-preview-view h1,
body.rbr-headings-accent-only .HyperMD-header-1,
body.rbr-headings-accent-only .cm-header-1,
body.rbr-headings-accent-only .inline-title {
color: var(--rbr-accent) !important;
}
/* Trio — RBR's default. Forced via direct selectors so external rules
(Minimal's `colorful-headings` leftover, plugin overrides, snippets)
can't collapse all headings to one color. */
body.rbr-headings-trio .markdown-rendered h1,
body.rbr-headings-trio .markdown-preview-view h1,
body.rbr-headings-trio .HyperMD-header-1,
body.rbr-headings-trio .cm-header-1,
body.rbr-headings-trio .inline-title {
color: var(--rbr-accent) !important;
}
body.rbr-headings-trio .markdown-rendered h2,
body.rbr-headings-trio .markdown-preview-view h2,
body.rbr-headings-trio .HyperMD-header-2,
body.rbr-headings-trio .cm-header-2 {
color: var(--rbr-rb-yellow) !important;
}
body.rbr-headings-trio .markdown-rendered h3,
body.rbr-headings-trio .markdown-preview-view h3,
body.rbr-headings-trio .HyperMD-header-3,
body.rbr-headings-trio .cm-header-3 {
color: var(--rbr-chequer-white) !important;
}
body.rbr-headings-trio .markdown-rendered h4,
body.rbr-headings-trio .markdown-preview-view h4,
body.rbr-headings-trio .HyperMD-header-4,
body.rbr-headings-trio .cm-header-4 {
color: var(--rbr-paddock-green) !important;
}
body.rbr-headings-trio .markdown-rendered h5,
body.rbr-headings-trio .markdown-preview-view h5,
body.rbr-headings-trio .HyperMD-header-5,
body.rbr-headings-trio .cm-header-5 {
color: var(--rbr-oracle-blue) !important;
}
body.rbr-headings-trio .markdown-rendered h6,
body.rbr-headings-trio .markdown-preview-view h6,
body.rbr-headings-trio .HyperMD-header-6,
body.rbr-headings-trio .cm-header-6 {
color: var(--rbr-sky-blue) !important;
}
/* ========================================================================
Inline markup — links, bold, italic, blockquote, hr
======================================================================== */
.theme-dark,
.theme-light {
--link-color: var(--rbr-oracle-blue);
--link-color-hover: var(--rbr-sky-blue);
--link-external-color: var(--rbr-livery-blue-bright);
--link-external-color-hover: var(--rbr-sky-blue);
--link-unresolved-color: var(--rbr-kerb-red-bright);
--link-unresolved-opacity: 0.85;
--link-unresolved-filter: none;
--bold-color: var(--rbr-chequer-white);
--bold-weight: 700;
--italic-color: var(--rbr-livery-blue-bright);
--blockquote-border-color: var(--rbr-accent);
--blockquote-background-color: rgba(var(--rbr-accent-rgb), 0.06);
--blockquote-color: var(--rbr-kerb-red-pure);
--hr-color: var(--rbr-overlay-0);
--hr-thickness: 2px;
}
/* Strikethrough — fade into subtext.
Obsidian has no dedicated variable for ~~text~~, so use direct selectors. */
.markdown-rendered del,
.markdown-rendered s,
.markdown-preview-view del,
.markdown-preview-view s,
.cm-s-obsidian .cm-strikethrough {
color: var(--rbr-subtext-0);
}
/* ========================================================================
Code — inline + block. Syntax tokens stay locked to RBR doctrine
regardless of the active accent (keeps code legible).
--code-size is driven by the --rbr-code-scale slider (default 0.92em).
======================================================================== */
.theme-dark,
.theme-light {
--code-background: var(--rbr-mantle);
--code-normal: var(--rbr-text);
--code-keyword: var(--rbr-kerb-red);
--code-string: var(--rbr-paddock-green);
--code-number: var(--rbr-light-grey);
--code-function: var(--rbr-chequer-white);
--code-property: var(--rbr-oracle-blue);
--code-tag: var(--rbr-kerb-red);
--code-comment: var(--rbr-subtext-0);
--code-operator: var(--rbr-subtext-1);
--code-punctuation: var(--rbr-subtext-1);
--code-value: var(--rbr-paddock-green);
--code-important: var(--rbr-rb-yellow);
--code-size: var(--rbr-code-scale, 0.92em);
}
/* CodeMirror live-preview / source-mode token overrides. */
.cm-s-obsidian span.cm-keyword,
.cm-s-obsidian span.cm-atom,
.cm-s-obsidian span.cm-builtin { color: var(--rbr-kerb-red); }
.cm-s-obsidian span.cm-string,
.cm-s-obsidian span.cm-string-2 { color: var(--rbr-paddock-green); }
.cm-s-obsidian span.cm-number { color: var(--rbr-light-grey); }
.cm-s-obsidian span.cm-def,
.cm-s-obsidian span.cm-property { color: var(--rbr-chequer-white); }
.cm-s-obsidian span.cm-variable { color: var(--rbr-text); }
.cm-s-obsidian span.cm-variable-2 { color: var(--rbr-sky-blue); }
.cm-s-obsidian span.cm-variable-3,
.cm-s-obsidian span.cm-type { color: var(--rbr-rb-yellow); }
.cm-s-obsidian span.cm-comment { color: var(--rbr-subtext-0); font-style: italic; }
.cm-s-obsidian span.cm-meta,
.cm-s-obsidian span.cm-qualifier { color: var(--rbr-sky-blue); }
.cm-s-obsidian span.cm-operator { color: var(--rbr-subtext-1); }
.cm-s-obsidian span.cm-tag { color: var(--rbr-kerb-red); }
.cm-s-obsidian span.cm-attribute { color: var(--rbr-rb-yellow); }
.cm-s-obsidian span.cm-bracket { color: var(--rbr-subtext-1); }
.cm-s-obsidian span.cm-link { color: var(--rbr-oracle-blue); }
.cm-s-obsidian span.cm-error { color: var(--rbr-kerb-red-bright); }
.cm-s-obsidian span.cm-header { color: var(--rbr-accent); }
.cm-s-obsidian span.cm-hr { color: var(--rbr-overlay-0); }
.cm-s-obsidian span.cm-quote { color: var(--blockquote-color); }
/* Live-preview / source-mode quote line — Obsidian wraps each quoted line
in .HyperMD-quote, which doesn't go through --blockquote-color by default.
Force it so Reading view and Live preview match. */
.cm-s-obsidian .HyperMD-quote,
.cm-s-obsidian .HyperMD-quote-1,
.cm-s-obsidian .HyperMD-quote-2,
.cm-s-obsidian .HyperMD-quote-3 {
color: var(--blockquote-color) !important;
}
/* Reading view enforcement — some plugins/snippets paint blockquote color
directly on the <blockquote> rather than letting --blockquote-color cascade */
.markdown-rendered blockquote,
.markdown-preview-view blockquote {
color: var(--blockquote-color) !important;
}
/* ========================================================================
Tables
======================================================================== */
.theme-dark,
.theme-light {
--table-background: transparent;
--table-header-background: var(--rbr-mantle);
--table-header-background-hover: var(--rbr-surface-0);
--table-row-background: transparent;
--table-row-alt-background: rgba(var(--rbr-base-rgb), 0.4);
--table-row-background-hover: var(--rbr-surface-1);
--table-border-color: var(--rbr-surface-1);
--table-header-color: var(--rbr-rb-yellow);
}
/* ========================================================================
Tags — small pill badges. Yellow because tags are a "look here" cue.
Foreground is forced to black for legibility against the yellow fill.
======================================================================== */
.theme-dark,
.theme-light {
--tag-color: #000000;
--tag-color-hover: #000000;
--tag-background: var(--rbr-rb-yellow);
--tag-background-hover: var(--rbr-rb-pure);
--tag-border-color: transparent;
--tag-border-color-hover: transparent;
--tag-radius: 10px;
--tag-weight: 600;
}
/* Direct selector enforcement — CodeMirror's hashtag spans inherit syntax
token rules (.cm-meta / .cm-tag) that ignore --tag-color. Force black
on the tag pill in reading view, live preview, and source mode. */
.tag,
a.tag,
.cm-s-obsidian span.cm-hashtag,
.cm-s-obsidian span.cm-hashtag-begin,
.cm-s-obsidian span.cm-hashtag-end,
.cm-s-obsidian span.cm-meta.cm-hashtag,
.cm-s-obsidian span.cm-tag.cm-hashtag {
color: #000000 !important;
background-color: var(--rbr-rb-yellow) !important;
font-weight: 600 !important;
}
.tag:hover,
a.tag:hover,
.cm-s-obsidian span.cm-hashtag:hover {
color: #000000 !important;
background-color: var(--rbr-rb-pure) !important;
}
/* ========================================================================
Checkboxes — follow the active accent
======================================================================== */
.theme-dark,
.theme-light {
--checkbox-color: var(--rbr-accent);
--checkbox-color-hover: var(--rbr-accent-hover);
--checkbox-border-color: var(--rbr-overlay-2);
--checkbox-border-color-hover: var(--rbr-accent);
--checkbox-marker-color: var(--rbr-base);
--checklist-done-color: var(--rbr-subtext-0);
--checklist-done-decoration: line-through;
}
/* ========================================================================
Modals, popovers, tooltips
======================================================================== */
.theme-dark,
.theme-light {
--modal-background: var(--rbr-surface-0);
--modal-border-color: var(--rbr-surface-2);
--modal-radius: 10px;
--popover-background: var(--rbr-surface-0);
--popover-border-color: var(--rbr-surface-2);
--tooltip-background: var(--rbr-mantle);
--tooltip-text-color: var(--rbr-text);
}
/* ========================================================================
Title bar / status bar
======================================================================== */
.theme-dark,
.theme-light {
--titlebar-background: var(--rbr-mantle);
--titlebar-background-focused: var(--rbr-mantle);
--titlebar-border-color: var(--rbr-surface-1);
--titlebar-text-color: var(--rbr-text);
--titlebar-text-color-focused: var(--rbr-text);
--titlebar-text-color-blurred: var(--rbr-subtext-1);
--status-bar-background: var(--rbr-mantle);
--status-bar-text-color: var(--rbr-subtext-1);
--status-bar-border-color: var(--rbr-surface-1);
}
/* ========================================================================
Tabs — base variables. Indicator style chosen by Style Settings below.
======================================================================== */
.theme-dark,
.theme-light {
--tab-background-active: var(--rbr-base);
--tab-text-color: var(--rbr-subtext-1);
--tab-text-color-active: var(--rbr-text);
--tab-text-color-focused: var(--rbr-text);
--tab-text-color-focused-active: var(--rbr-text);
--tab-text-color-focused-active-current: var(--rbr-text);
--tab-text-color-active-inactive-current: var(--rbr-subtext-1);
--tab-divider-color: var(--rbr-surface-1);
--tab-outline-color: var(--rbr-surface-1);
--tab-container-background: var(--rbr-mantle);
}
/* Reset all indicators by default — only the chosen style draws */
.workspace-tab-header.is-active::before {
background-color: transparent;
}
/* Top border (default) */
body.rbr-tab-top .workspace-tab-header.is-active::before {
background-color: var(--rbr-accent);
top: 0;
bottom: auto;
height: 2px;
}
/* Bottom border */
body.rbr-tab-bottom .workspace-tab-header.is-active::before {
background-color: var(--rbr-accent);
top: auto;
bottom: 0;
height: 2px;
}
/* Background fill — soft accent-tinted fill, no border bar */
body.rbr-tab-fill .workspace-tab-header.is-active {
background-color: rgba(var(--rbr-accent-rgb), 0.18);
}
body.rbr-tab-fill .workspace-tab-header.is-active::before {
background-color: transparent;
}
/* ========================================================================
Scrollbars
======================================================================== */
.theme-dark,
.theme-light {
--scrollbar-bg: transparent;
--scrollbar-thumb-bg: var(--rbr-surface-2);
--scrollbar-active-thumb-bg: var(--rbr-accent);
}
/* ========================================================================
Search highlights — alpha follows --rbr-highlight-alpha slider
======================================================================== */
.search-result-file-matched-text,
.search-result-file-matched-text-highlighted {
background-color: rgba(var(--rbr-rb-yellow-rgb), var(--rbr-highlight-alpha, 0.30)) !important;
color: var(--rbr-rb-yellow) !important;
border-radius: 3px;
}
.cm-s-obsidian span.obsidian-search-match-highlight {
background-color: rgba(var(--rbr-rb-yellow-rgb), var(--rbr-highlight-alpha, 0.30));
border-bottom: 1px solid var(--rbr-rb-yellow);
}
/* ========================================================================
Graph view — node colors map to file states
======================================================================== */
.theme-dark,
.theme-light {
--graph-line: var(--rbr-overlay-0);
--graph-node: var(--rbr-text);
--graph-node-tag: var(--rbr-rb-yellow);
--graph-node-attachment: var(--rbr-paddock-green);
--graph-node-unresolved: var(--rbr-kerb-red-bright);
--graph-node-focused: var(--rbr-accent);
--graph-text: var(--rbr-text);
}
/* ========================================================================
Callouts — semantic colors lock to RBR diagnostics, ignore accent choice
======================================================================== */
.theme-dark,
.theme-light {
--callout-info: var(--rbr-oracle-blue-rgb);
--callout-tip: var(--rbr-paddock-green-rgb);
--callout-success: var(--rbr-paddock-green-rgb);
--callout-question: 130, 160, 216;
--callout-warning: var(--rbr-rb-yellow-rgb);
--callout-todo: var(--rbr-rb-yellow-rgb);
--callout-bug: var(--rbr-kerb-bright-rgb);
--callout-fail: var(--rbr-kerb-bright-rgb);
--callout-error: var(--rbr-kerb-bright-rgb);
--callout-example: var(--rbr-paddock-green-rgb);
--callout-quote: 133, 144, 174;
--callout-abstract: var(--rbr-oracle-blue-rgb);
--callout-note: var(--rbr-oracle-blue-rgb);
--callout-important: var(--rbr-kerb-red-rgb);
--callout-cite: 133, 144, 174;
}
/* ========================================================================
File explorer — current file gets an active-accent bar on the left
======================================================================== */
.nav-file-title.is-active,
.nav-folder-title.is-active {
color: var(--rbr-text);
background-color: var(--rbr-surface-1);
position: relative;
}
.nav-file-title.is-active::before,
.nav-folder-title.is-active::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background-color: var(--rbr-accent);
}
.nav-file-title.has-focus,
.nav-folder-title.has-focus {
background-color: var(--rbr-surface-2);
}
/* ========================================================================
Mobile — keep the trio working on small screens
======================================================================== */
.is-mobile {
--background-primary: var(--rbr-base);
--background-secondary: var(--rbr-mantle);
}
/* ========================================================================
Small refinements — focus ring, drag handle, slider, vault name
======================================================================== */
.theme-dark,
.theme-light {
--slider-thumb-border-active: var(--rbr-accent);
--slider-track-background: var(--rbr-surface-1);
--slider-thumb-background: var(--rbr-text);
--toggle-thumb-color: var(--rbr-text);
--toggle-border-color: var(--rbr-overlay-1);
--toggle-bg-color: var(--rbr-surface-1);
--toggle-on-bg-color: var(--rbr-accent);
--toggle-on-thumb-color: var(--rbr-base);
--vault-name-color: var(--rbr-rb-yellow);
--vault-name-font-weight: 700;
}
/* Active line — STYLE-GUIDE: surface1 (dimmed navy, one step above base).
Quiet "you are here" without yelling. Cover both class orderings + the
CM6 single-class form, plus the matching gutter line. */
.cm-line.cm-active,
.cm-active.cm-line,
.cm-activeLine {
background-color: var(--rbr-surface-1) !important;
}
.cm-activeLineGutter,
.cm-gutters .cm-activeLineGutter {
background-color: var(--rbr-surface-1) !important;
color: var(--rbr-text);
}
/* Selected text — alpha-blended cursor color so vim visual mode +
regular drag-select share the same accent. 30% alpha keeps the
characters under the selection legible. Reading-view text selection
(--text-selection) stays on the active accent — that path doesn't
go through CM6 and shouldn't share the cursor color. */
.cm-s-obsidian .cm-selectionBackground,
.cm-s-obsidian .CodeMirror-selected,
.cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
background-color: rgba(var(--rbr-cursor-rgb), 0.30) !important;
}
/* When the editor isn't focused, dim the selection a bit more so it doesn't
compete visually with the active pane */
.cm-editor:not(.cm-focused) .cm-selectionBackground {
background-color: rgba(var(--rbr-cursor-rgb), 0.18) !important;
}
/* Matching brackets — follow active accent */
.cm-s-obsidian .CodeMirror-matchingbracket {
color: var(--rbr-accent) !important;
border-bottom: 1px solid var(--rbr-accent);
}
/* ========================================================================
Calendar / properties panel / inline title
======================================================================== */
.inline-title {
color: var(--rbr-accent);
font-weight: 800;
}
.metadata-property-key-input {
color: var(--rbr-rb-yellow);
}
.metadata-property-value {
color: var(--rbr-text);
}
/* ========================================================================
Auto hide — Style Settings class-toggles. Reduce UI chrome to a sliver
and reveal on hover. Mobile is excluded since it has no hover affordance.
Each toggle is independent — combine freely (e.g. ribbon + statusbar).
Perf notes:
- All durations route through --rbr-autohide-duration so users can dial
them down (default 100ms). Animating height/width is unavoidable here
because the surrounding layout must reflow — but a shorter window
shrinks the cost proportionally.
- `contain: layout` is applied to each region (see the rule directly
below) so reflow stays inside the region and doesn't bubble up to
the workspace tree on every mousemove or typing-time DOM mutation.
- `will-change` is applied **only on :hover**, not always-on. Permanent
will-change keeps a composite layer allocated forever, which makes
the compositor redo work on every paint — including paints triggered
by CodeMirror DOM updates while typing. By gating it behind :hover
we trade a tiny first-frame layer-allocation cost for typing-time
compositor calm.
- The ribbon keeps the longer Material curve for a heftier slide — its
width swing is the largest of the four regions, so a snappier
duration would look abrupt. Other regions get the linear ease-out.
======================================================================== */
/* Reflow isolation. `contain: layout` walls off the auto-hide region's
reflow so it doesn't propagate up through the workspace tree on every
mousemove or DOM mutation. `style` is intentionally NOT included — it
can interfere with custom-property inheritance (notably --ribbon-width
via @property), and the layout containment alone is what we want.
`will-change` is NOT applied here. Always-on `will-change` keeps a
composite layer permanently allocated, which forces the compositor to
redo layer work on every paint — including paints driven by
CodeMirror's typing-time DOM mutations. We instead promote the layer
only when a hover is actually in flight (see the :hover rules per
region) so the GPU layer exists only while it's needed. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container,
body:not(.is-mobile).rbr-autohide-statusbar .status-bar,
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left,
body:not(.is-mobile).rbr-autohide-vault-profile .workspace-sidedock-vault-profile {
contain: layout;
}
/* will-change is set only when the region is actually about to animate
(i.e. the user has hovered it). Setting will-change at :hover allocates
the composite layer just-in-time; when hover ends, the property is
removed and the layer is released. This keeps typing fast because the
editor pane no longer competes with always-promoted sibling layers. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container:hover {
will-change: height;
}
body:not(.is-mobile).rbr-autohide-statusbar .status-bar:hover {
will-change: height;
}
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left:hover {
will-change: width;
}
body:not(.is-mobile).rbr-autohide-vault-profile .workspace-split.mod-left-split:hover .workspace-sidedock-vault-profile,
body:not(.is-mobile).rbr-autohide-vault-profile .workspace-sidedock-vault-profile:hover {
will-change: height;
}
/* ---- Tab bar ----
Collapse every tab header strip (main + left/right sidebars) into a
slim 16px bar. Following Obsidian-Border's design: the strip itself
is the hover target — no pseudo-element hit zones, no overflow
overrides, no pointer-events juggling. Simpler = smoother.
Trade-off: the hover target is exactly the 16px strip (plus the
titlebar via the secondary trigger below). No "generous band"
above/below. In exchange, transitions feel snappy because the
browser isn't recomputing hit-testing over giant viewport-wide
pseudo-element overlays on every frame.
Each strip expands independently on its own :hover (this used to be
a synchronized expand via `:has()`, but measurement showed the
`:has()` selectors caused thousands of editor nodes to re-style on
every keystroke — see the expand rule below for the full rationale).
The faint accent hairline at the bottom of the collapsed strip is
a visible "aim here" cue (otherwise a 16px transparent area is
easy to miss). */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container {
position: relative;
height: 12px;
min-height: 12px;
overflow: visible;
border-bottom: 1px solid rgba(var(--rbr-accent-rgb), 0.35);
/* Only transition height + min-height. border-color is left to
change instantly (the hairline disappears on expand) — eliminating
it from the transition list shaves work off every animation frame. */
transition: height var(--rbr-autohide-duration, 100ms) ease-out,
min-height var(--rbr-autohide-duration, 100ms) ease-out;
transition-delay: 0ms; /* collapse direction: snap back immediately */
}
/* Invisible hover-extension above the strip — the strip itself is only
12px visible, but the ::before adds an invisible band above so the
effective hit zone is taller. Height is controlled via
--rbr-autohide-hitzone (Style Settings slider) so it's dial-able
per-user — bigger zone = easier to aim, smaller = fewer accidental
expands. Default 12px → total 24px effective hit zone.
left/right -100vw is kept for vertical-stack visual continuity, but
the lateral extension is now clipped at the panel boundary (we no
longer set overflow:visible on .workspace-split — that was expanding
the editor's paint damage rect). The band still meets the titlebar at
the top, which is the primary hover target.
No z-index, no pointer-events tricks, no dynamic resize — just one
thin static band per strip. Each strip's :hover triggers only that
strip's expand (synchronized expand was dropped — see expand rule). */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container::before {
content: "";
position: absolute;
/* Bounded to the panel's own width — `-100vw` extension was removed
after a cross-panel bug: with side panel splits now overflow:visible
for the vertical band, `-100vw` lateral extension let the right
sidebar's pseudo reach the left ribbon area, so hovering near the
ribbon was firing the right sidebar's :hover. The viewport-wide
top hit zone is owned by the titlebar pseudo (see below); each
strip's own pseudo stays within its panel. */
left: 0;
right: 0;
top: calc(-1 * var(--rbr-autohide-hitzone, 12px));
height: var(--rbr-autohide-hitzone, 12px);
}
/* `.workspace-tabs.mod-top` (the strip's direct parent) is opened so the
strip's ::before can extend vertically above it.
Side panel splits (mod-left-split, mod-right-split) ALSO get opened
so the side panel strip's ::before extends laterally beyond the
panel boundary — this brings the side panel hit zone in line with
the main editor's. The main editor split (.mod-root) is intentionally
NOT opened: it's the editor's ancestor, and `overflow: visible` there
would expand CodeMirror's paint damage rect on every keystroke. The
main editor's wide top hit zone is handled by the titlebar pseudo
below instead, which lives outside .mod-root entirely. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tabs.mod-top,
body:not(.is-mobile).rbr-autohide-tabs .workspace-split.mod-left-split,
body:not(.is-mobile).rbr-autohide-tabs .workspace-split.mod-right-split {
overflow: visible;
}
/* Raise the strip's wrapper above the content area's stacking. The
strip will get a ::after pseudo that extends DOWN into the content
area (file explorer / editor / etc.), and that pseudo needs to be
on top of the content for hover hit-testing to fire. Without this,
the content (rendered later in DOM order) wins the hit-test and the
pseudo's hover never triggers. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tabs.mod-top {
position: relative;
z-index: 10;
}
/* Below-strip hit zone — mirrors ::before above. Same hitzone slider
controls the height. This covers the area where the tab buttons
(Files/Search/Bookmarks/...) would VISUALLY APPEAR when the strip
is expanded, so hovering that spot triggers the expand even though
the strip is collapsed.
Trade: clicks inside this band are captured by the strip, not the
content beneath (file in file explorer, top line of editor, etc.).
At 12px default this is barely noticeable; at 100px the capture
reaches deep into the content area — dial accordingly. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container::after {
content: "";
position: absolute;
/* See the ::before rule above for why we bound to panel width
(`left: 0; right: 0`) instead of `-100vw`. */
left: 0;
right: 0;
top: 100%;
height: var(--rbr-autohide-hitzone, 12px);
}
/* ---- Window-wide top hit zone (titlebar-anchored) ----
The strips themselves are clipped by their respective workspace
splits. We get a generous full-width top hit zone by adding an
invisible band on the titlebar — .titlebar sits OUTSIDE all the
workspace splits, at the very top of the window, so its pseudo
isn't clipped by any of them.
How it works:
1. .titlebar gets position: relative + overflow: visible so its
::after can extend downward past its own bounds.
2. The ::after is an absolutely-positioned band immediately below
the titlebar, full window width, height controlled by the same
--rbr-autohide-hitzone slider that controls the per-strip zones.
3. The pseudo is part of the titlebar element for hit-testing, so
hovering it triggers .titlebar:hover.
4. The .titlebar:hover ~ * .workspace-tab-header-container rule
uses a general-sibling combinator to expand all three strips
(main + left/right sidebars) at once. Sibling combinators don't
have the body-level :has() invalidation cost we removed earlier,
so this synchronized expand is perf-safe.
Trade: clicks inside the pseudo band are captured by the titlebar
instead of falling through to whatever's underneath. With a
moderate hit zone height (1240px) this is invisible in practice
since the area is the titlebar/workspace top. At 100px the band
reaches deep into the workspace and click capture becomes
noticeable — dial accordingly. */
body:not(.is-mobile).rbr-autohide-tabs .titlebar {
position: relative;
overflow: visible;
}
body:not(.is-mobile).rbr-autohide-tabs .titlebar::after {
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: var(--rbr-autohide-hitzone, 12px);
z-index: 100;
}
body:not(.is-mobile).rbr-autohide-tabs .titlebar:hover ~ * .workspace-tab-header-container {
height: var(--header-height, 36px);
min-height: var(--header-height, 36px);
border-bottom-color: transparent;
transition-delay: var(--rbr-autohide-delay, 200ms);
}
body:not(.is-mobile).rbr-autohide-tabs .titlebar:hover ~ * .workspace-tab-header-container > * {
opacity: 1;
pointer-events: auto;
transition: opacity var(--rbr-autohide-duration, 100ms) ease-out calc(var(--rbr-autohide-delay, 200ms) + 30ms);
}
/* Inner button row fades. pointer-events:none is needed because
overflow:visible lets the (full-height) buttons render past the
12px clip line — without it, hovers on those overflowing buttons
would fire phantom expands. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container > * {
opacity: 0;
pointer-events: none;
transition: opacity var(--rbr-autohide-duration, 100ms) ease-out 0ms;
}
/* Expand on hover — each strip responds to its OWN :hover state. The
"synchronized expand" behavior (hover any one strip → all three open)
was implemented with `:has()` selectors, but measurement showed those
selectors caused the browser to re-evaluate style for thousands of
editor DOM nodes on every CodeMirror keystroke. The trade is real but
clear: drop synchronization (each tab strip expands alone), keep
typing fast. This is the same behavior Border ships with — the design
we're inspired by — so the trade is well-precedented.
The transition-delay is the hover-intent gate: when the cursor first
enters the hover state, transitions wait --rbr-autohide-delay before
kicking in. If the cursor leaves before that window elapses, the
transitions never start — no flicker from drive-by hovers. On exit,
the default rules above apply (0ms delay), so collapse happens
immediately. */
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container:hover {
height: var(--header-height, 36px);
min-height: var(--header-height, 36px);
border-bottom-color: transparent;
transition-delay: var(--rbr-autohide-delay, 200ms);
}
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container:hover > * {
opacity: 1;
pointer-events: auto;
transition: opacity var(--rbr-autohide-duration, 100ms) ease-out calc(var(--rbr-autohide-delay, 200ms) + 30ms);
}
/* ---- Status bar ---- */
body:not(.is-mobile).rbr-autohide-statusbar .status-bar {
height: 4px;
opacity: 0;
overflow: hidden;
transition: height var(--rbr-autohide-duration, 100ms) ease-out,
opacity var(--rbr-autohide-duration, 100ms) ease-out;
transition-delay: 0ms;
}
body:not(.is-mobile).rbr-autohide-statusbar .status-bar:hover {
height: 24px;
opacity: 1;
transition-delay: var(--rbr-autohide-delay, 200ms);
}
/* ---- Left ribbon ----
Drive ribbon width via the --ribbon-width CSS variable so Obsidian's
flex layout reflows alongside (a direct `width:` would shrink the
ribbon visually but leave the column reserved).
For smooth motion we register --ribbon-width as a typed @property.
By default custom properties aren't transitionable — declaring the
syntax as <length> tells the engine to interpolate it numerically,
so changing the value over a `transition` produces a fluid slide
instead of an instant snap.
Easing: cubic-bezier(0.4, 0, 0.2, 1) is the Material "standard" curve
— slow start, snappy middle, gentle landing. Feels natural for this
kind of UI panel. Icons fade in slightly delayed so the ribbon has
already started widening when they appear. */
/* Critical: inherits is FALSE. A typed custom property registered with
`inherits: true` participates in the cascade for every descendant of
the element it's set on. Setting it on body meant CodeMirror's 1000+
.cm-line nodes paid a per-node cascade cost on every style recalc —
which CodeMirror triggers constantly during typing. With `inherits:
false`, the property is scoped to the element it's declared on
(ribbon), and the editor subtree no longer sees it during recalc. */
@property --ribbon-width {
syntax: '<length>';
inherits: false;
initial-value: 44px;
}
/* --ribbon-width is declared directly on the ribbon element (not body)
so the transition runs on the element that actually consumes the
value. background-color is dropped from the transition list; it had
no visible payoff and was compositing on every frame. Ribbon slides
38px (6→44) so we scale the duration up by 1.6× — keeps the curve
readable while smaller-motion regions stay snappy. */
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left {
--ribbon-width: 6px;
/* position: relative anchors the ::before hit-zone overlay below. */
position: relative;
/* overflow: visible lets the ::before extend past the ribbon's own
6px width into the editor area on the right. */
overflow: visible;
transition: --ribbon-width calc(var(--rbr-autohide-duration, 100ms) * 1.6) cubic-bezier(0.4, 0, 0.2, 1),
width calc(var(--rbr-autohide-duration, 100ms) * 1.6) cubic-bezier(0.4, 0, 0.2, 1),
min-width calc(var(--rbr-autohide-duration, 100ms) * 1.6) cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: 0ms;
}
/* Invisible hover-extension to the right of the ribbon — width is
controlled via --rbr-autohide-ribbon-hitzone (Style Settings slider).
The pseudo overlays the editor area immediately to the right of the
collapsed ribbon; hovering anywhere in this band fires the ribbon's
:hover. Trade: clicks inside the band also go to the ribbon, not the
editor, so dial it modestly (2032px is usually plenty). */
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: var(--rbr-autohide-ribbon-hitzone, 20px);
}
/* On hover, restore the default Obsidian width (44px) so icons fit.
The hover-intent delay below means a drive-by hover doesn't trigger
the expand — only intentional dwelling does. */
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left:hover {
--ribbon-width: 44px;
transition-delay: var(--rbr-autohide-delay, 200ms);
}
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left > * {
opacity: 0;
transition: opacity calc(var(--rbr-autohide-duration, 100ms) * 1.2) ease-out 0ms;
}
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left:hover > * {
opacity: 1;
transition: opacity calc(var(--rbr-autohide-duration, 100ms) * 1.2) ease-out calc(var(--rbr-autohide-delay, 200ms) + 80ms);
}
/* ---- Vault profile ----
Hide the vault-name footer in the left sidebar. Reveal when the sidebar
pane itself is hovered (the bar is too thin to land on directly). */
body:not(.is-mobile).rbr-autohide-vault-profile .workspace-sidedock-vault-profile {
height: 0;
opacity: 0;
overflow: hidden;
padding-top: 0;
padding-bottom: 0;
/* padding isn't in the transition list — it's clipped by overflow:hidden
until height opens up, so an instant padding snap is invisible. */
transition: height var(--rbr-autohide-duration, 100ms) ease-out,
opacity var(--rbr-autohide-duration, 100ms) ease-out;
transition-delay: 0ms;
}
body:not(.is-mobile).rbr-autohide-vault-profile .workspace-split.mod-left-split:hover .workspace-sidedock-vault-profile,
body:not(.is-mobile).rbr-autohide-vault-profile .workspace-sidedock-vault-profile:hover {
height: auto;
opacity: 1;
padding-top: var(--size-4-2, 8px);
padding-bottom: var(--size-4-2, 8px);
transition-delay: var(--rbr-autohide-delay, 200ms);
}
/* ========================================================================
Card layout — give every workspace pane card-like separation:
gaps between splits, rounded corners, and a soft shadow on each tab
container. The shadow layers a 1px border on top of a subtle drop
shadow for depth without harshness.
Sliders (--rbr-card-radius, --rbr-card-gap) drive the metrics so users
can dial it from "barely there" to "tile UI" without editing CSS.
======================================================================== */
/* Gap between vertically stacked splits (panes side-by-side) */
body:not(.is-mobile).rbr-card-layout .workspace-split.mod-vertical {
gap: var(--rbr-card-gap, 12px);
}
/* Sidedock margins so cards don't touch the ribbon or right sidebar */
body:not(.is-mobile).rbr-card-layout .workspace.is-left-sidedock-open .workspace-split.mod-horizontal.mod-left-split {
margin-right: var(--rbr-card-gap, 12px);
}
body:not(.is-mobile).rbr-card-layout .workspace.is-right-sidedock-open .workspace-split.mod-horizontal.mod-right-split {
margin-left: var(--rbr-card-gap, 12px);
}
/* The card itself — rounded + shadowed. overflow:hidden clips content
to the radius; the parent .workspace-tabs / .workspace-split must stay
overflow:visible so the drop shadow isn't cut off. */
body:not(.is-mobile).rbr-card-layout .workspace-tab-container {
border-radius: var(--rbr-card-radius, 8px) !important;
overflow: hidden !important;
box-shadow:
0 0 0 1px var(--rbr-surface-1),
0 4px 14px rgba(0, 0, 0, 0.28);
}
body:not(.is-mobile).rbr-card-layout .workspace-tabs,
body:not(.is-mobile).rbr-card-layout .workspace-split {
overflow: visible !important;
}
/* Tab strip blends into the canvas instead of looking like a chrome strip
on top of the card */
body:not(.is-mobile).rbr-card-layout .workspace-tabs,
body:not(.is-mobile).rbr-card-layout .workspace-tab-header-container {
background-color: transparent !important;
}
/* The default ::before drop-shadow line under the title bar is meant for
the flat layout — hide it once cards take over that role */
body:not(.is-mobile).rbr-card-layout .workspace::before {
display: none;
}
/* Active tab card — the active leaf gets a hint of accent on the border
so it's still distinguishable when every pane has a card outline */
body:not(.is-mobile).rbr-card-layout .workspace-tabs.mod-active .workspace-tab-container {
box-shadow:
0 0 0 1px rgba(var(--rbr-accent-rgb), 0.45),
0 4px 14px rgba(0, 0, 0, 0.32);
}