amdhj22_rbr.obsidian/theme.css
Amdhj22 7d2908515d fix(autohide): match hover hit zones to expanded size
- lift hit-zone stacking above the side dock (sidedock+1) so open
  panels no longer cover the ribbon's hover band
- size each hover zone to its expanded region: ribbon width, tab
  --header-height, status bar height
- keep icons/content above the bands so wider zones don't eat clicks
- drop ribbon double-interpolation (animate --ribbon-width only)
- speed up defaults: hover delay 200->100ms, duration 100->80ms
2026-07-08 19:40:45 +09:00

1656 lines
65 KiB
CSS
Raw Permalink 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 = 즉시 펼침, 100 = 살짝만 확인(기본), 400+ = 의도적으로 머무를 때만.
type: variable-number-slider
default: 100
min: 0
max: 600
step: 50
format: ms
-
id: rbr-autohide-duration
title: Expand/collapse animation duration
description: |
자동 숨김 영역이 펼쳐지고 닫히는 애니메이션 길이 (ms).
짧을수록 가볍고 스냅함. 기본 80ms. 더 스냅하게 60까지 내릴 수 있음.
layout-property transition 비용을 줄여 마우스 hover 시
메인 스레드 부담을 직접 낮춤.
type: variable-number-slider
default: 80
min: 60
max: 280
step: 20
format: ms
-
id: rbr-autohide-hitzone
title: Tab strip top hover zone (above strip)
description: |
접힌 탭 strip 위쪽(타이틀바 방향) hover 영역의 높이 (px).
아래쪽 hover 영역은 "펼친 탭 높이"에 자동으로 맞춰지므로 이 값과
무관하다 — 이 슬라이더는 위쪽 밴드/타이틀바 트리거 높이만 조절.
메인 에디터/좌우 사이드 패널 strip 모두에 적용.
type: variable-number-slider
default: 12
min: 4
max: 100
step: 4
format: px
-
id: rbr-ribbon-expanded
title: Expanded ribbon width (= hover hit zone)
description: |
접힌 ribbon 위로 마우스를 올렸을 때 펼쳐지는 폭 (px).
hover hit zone 이 이 값과 항상 동일하게 맞춰짐 — 즉 "펼쳐질 리본이
차지할 자리"가 그대로 hover 영역이 된다. 그 자리에 마우스를 올리면
리본이 딱 그 자리로 펼쳐짐.
값을 키우면 리본이 더 넓게 펼쳐지면서 hover 영역도 같이 넓어짐.
아이콘은 hit zone 밴드 위에 있어서 폭을 키워도 클릭은 안 막힘.
주의: 접힌 6px 를 넘는 hover 밴드 구간의 클릭은 ribbon 으로 캡처됨.
type: variable-number-slider
default: 44
min: 32
max: 200
step: 4
format: px
-
id: rbr-statusbar-expanded
title: Expanded status bar height (= hover hit zone)
description: |
상태 바가 hover 시 펼쳐지는 높이 (px). hover hit zone 이 이 값과
동일하게 맞춰져 "펼쳐질 자리"가 그대로 hover 영역이 된다 — 리본과
같은 방식. 값을 키우면 상태 바가 더 높게 펼쳐지고 hover 영역도
같이 넓어짐.
type: variable-number-slider
default: 24
min: 12
max: 120
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 {
/* The animated property is --ribbon-width, not width — hint that one
so the value is right. (Layout-driving props get limited compositor
benefit either way, but the hint should at least name what moves.) */
will-change: --ribbon-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;
/* z-index lifts the strip (and its ::after hit-zone band) above the
tab CONTENT that sits below it in the SAME .workspace-tabs.mod-top.
The mod-top z-index:10 below only raises the whole strip+content
group over OTHER splits — it does nothing for the header-vs-content
order inside this group, where the content renders later in DOM and
would otherwise win the hit-test over the ::after band (measured:
the band was 100% covered without this). Verified against real
app.css: content leaves are z-auto, so 2 clears them. */
z-index: 2;
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, 80ms) ease-out,
min-height var(--rbr-autohide-duration, 80ms) 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 — locked to the EXPANDED strip height
(--header-height) rather than the small hitzone slider, so the band
covers exactly the area the tab buttons (Files/Search/Bookmarks/...)
will occupy once expanded. Hover the spot the strip is about to fill
and it opens into that spot — the same "hit zone == expanded size"
behaviour the ribbon uses.
Trade: clicks inside this band are captured by the strip, not the
content beneath (file in file explorer, top line of editor). The band
is only as deep as the expanded header, which is exactly the region a
user reaching for tab buttons expects to be interactive anyway. */
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(--header-height, 40px);
}
/* ---- 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, 40px);
min-height: var(--header-height, 40px);
border-bottom-color: transparent;
transition-delay: var(--rbr-autohide-delay, 100ms);
}
body:not(.is-mobile).rbr-autohide-tabs .titlebar:hover ~ * .workspace-tab-header-container > * {
opacity: 1;
pointer-events: auto;
transition: opacity var(--rbr-autohide-duration, 80ms) ease-out calc(var(--rbr-autohide-delay, 100ms) + 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, 80ms) 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, 40px);
min-height: var(--header-height, 40px);
border-bottom-color: transparent;
transition-delay: var(--rbr-autohide-delay, 100ms);
}
body:not(.is-mobile).rbr-autohide-tabs .workspace-tab-header-container:hover > * {
opacity: 1;
pointer-events: auto;
transition: opacity var(--rbr-autohide-duration, 80ms) ease-out calc(var(--rbr-autohide-delay, 100ms) + 30ms);
}
/* ---- Status bar ----
Collapse to a 4px sliver; reveal on hover. The hover hit zone is a
::before band ABOVE the sliver, exactly as tall as the expanded bar
(--rbr-statusbar-expanded), so you aim at the space the bar will fill
— the same "hit zone == expanded size" pattern as the ribbon/tab bar.
overflow is visible (not hidden) so the ::before can escape the 4px
box; the collapsed items are kept from stealing clicks via
pointer-events:none below. opacity:0 hides the sliver visually while
still letting the pseudo hit-test (opacity doesn't disable hit-test).
The bar is z-index:var(--layer-status-bar)==15 by default, above the
editor content (0), so the band is never covered. */
body:not(.is-mobile).rbr-autohide-statusbar .status-bar {
height: 4px;
opacity: 0;
overflow: visible;
transition: height var(--rbr-autohide-duration, 80ms) ease-out,
opacity var(--rbr-autohide-duration, 80ms) ease-out;
transition-delay: 0ms;
}
/* Collapsed items must not capture clicks in the sliver / overflow. */
body:not(.is-mobile).rbr-autohide-statusbar .status-bar > * {
pointer-events: none;
}
/* Hover hit zone: band above the sliver, height == expanded bar. */
body:not(.is-mobile).rbr-autohide-statusbar .status-bar::before {
content: "";
position: absolute;
left: 0;
right: 0;
/* Sit directly ABOVE the bar regardless of its collapsed height:
top:0 is the bar's top edge, translateY(-100%) lifts the band up by
its own height. Anchoring to bottom:0 failed because the collapsed
bar isn't a predictable 4px, so the band stayed buried inside it. */
top: 0;
transform: translateY(-100%);
height: var(--rbr-statusbar-expanded, 24px);
}
body:not(.is-mobile).rbr-autohide-statusbar .status-bar:hover {
height: var(--rbr-statusbar-expanded, 24px);
opacity: 1;
transition-delay: var(--rbr-autohide-delay, 100ms);
}
body:not(.is-mobile).rbr-autohide-statusbar .status-bar:hover > * {
pointer-events: auto;
}
/* ---- 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;
/* Expanded ribbon width lives in --rbr-ribbon-expanded (Style Settings
slider, fallback 44px). It is NOT declared here on purpose: declaring
it on this element would out-specify the body-level value Style
Settings injects, so the slider would do nothing. We only READ it
(via var()) in the :hover width and the ::before hit-zone below.
Because both read the same variable, the hover zone always matches
EXACTLY the space the expanded ribbon fills — hover the spot the
ribbon is about to occupy and it expands into that exact spot. */
/* position: relative anchors the ::before hit-zone overlay below.
z-index must sit ONE ABOVE the side panels. Obsidian's ribbon AND
its side docks both default to z-index:var(--layer-sidedock) (==10).
When the left sidebar is open, its split shares that layer, and
since it renders LATER in DOM order it wins the hover hit-test over
our ::before band — collapsing the effective zone to the ribbon's
own ~6px (the "only a thin sliver is hoverable" bug). A plain
`z-index:10` did nothing because it equalled the default. Bumping
to sidedock+1 lifts the ribbon (and its band) just above the side
panel while staying below status-bar(15)/popover(30)/menu(65)/
modal(50)/tooltip(70), so those still layer over the ribbon.
Verified against the real Obsidian app.css + workspace DOM. */
position: relative;
z-index: calc(var(--layer-sidedock, 10) + 1);
/* overflow: visible lets the ::before extend past the ribbon's own
6px width into the editor area on the right. */
overflow: visible;
/* ONLY animate --ribbon-width. Obsidian derives both width and
flex-basis from it (width: var(--ribbon-width); flex: 0 0 var(...)),
so animating the variable slides everything in lock-step. The old
rule ALSO transitioned `width` and `min-width`: that made width lag
behind the (non-transitioned) flex-basis, so the two moved on
different curves — a subtle jitter — while width had no visual
effect anyway (flex-basis wins sizing under flex:0 0). Dropping them
removes the double-interpolation and the wasted per-frame work. */
transition: --ribbon-width calc(var(--rbr-autohide-duration, 80ms) * 1.6) cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: 0ms;
}
/* Invisible hover-extension to the right of the ribbon. Its width is
locked to --rbr-ribbon-expanded — i.e. the hover zone is exactly as
wide as the ribbon becomes when expanded. Hovering the area the ribbon
will fill triggers the expand, and the ribbon then lands right there.
z-index below the ribbon's icons (they get z-index:2, see the `> *`
rule) but above the neighbour panel — so the band overlays the
editor/sidebar for hit-testing yet never swallows icon clicks on the
expanded ribbon.
Trade: clicks in the band's overflow (the part past the 6px collapsed
ribbon, over the editor/sidebar) go to the ribbon, not that panel. */
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-ribbon-expanded, 44px);
z-index: 1;
}
/* On hover, expand to --rbr-ribbon-expanded (same variable the hit-zone
uses) so icons fit and the ribbon fills exactly the hovered zone.
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: var(--rbr-ribbon-expanded, 44px);
transition-delay: var(--rbr-autohide-delay, 100ms);
}
/* position:relative + z-index:2 stacks the ribbon's real children (icons,
collapse button) ABOVE the ::before hit-zone band. Without this a wide
band would paint over the expanded icons and steal their clicks. With
it, the band is purely a hover catcher in the empty gutter and the
icons stay fully clickable no matter how far the band extends. */
body:not(.is-mobile).rbr-autohide-ribbon .workspace-ribbon.side-dock-ribbon.mod-left > * {
position: relative;
z-index: 2;
opacity: 0;
transition: opacity calc(var(--rbr-autohide-duration, 80ms) * 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, 80ms) * 1.2) ease-out calc(var(--rbr-autohide-delay, 100ms) + 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, 80ms) ease-out,
opacity var(--rbr-autohide-duration, 80ms) 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, 100ms);
}
/* ========================================================================
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);
}