pocketbook 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +308 -0
  4. data/bin/pocketbook +5 -0
  5. data/bin/test-render +49 -0
  6. data/lib/pocketbook/book.rb +28 -0
  7. data/lib/pocketbook/book_renderer/chapter.rb +85 -0
  8. data/lib/pocketbook/book_renderer/front_matter.rb +27 -0
  9. data/lib/pocketbook/book_renderer/metadata.rb +70 -0
  10. data/lib/pocketbook/book_renderer/pdf.rb +42 -0
  11. data/lib/pocketbook/book_renderer/toc.rb +55 -0
  12. data/lib/pocketbook/book_renderer.rb +140 -0
  13. data/lib/pocketbook/book_template.rb +40 -0
  14. data/lib/pocketbook/cli/options_parser.rb +344 -0
  15. data/lib/pocketbook/cli/runner.rb +505 -0
  16. data/lib/pocketbook/cli/watch_command.rb +275 -0
  17. data/lib/pocketbook/cli.rb +12 -0
  18. data/lib/pocketbook/core_stylesheet.rb +20 -0
  19. data/lib/pocketbook/pdf_document.rb +96 -0
  20. data/lib/pocketbook/render_request.rb +67 -0
  21. data/lib/pocketbook/styles/core/01_tokens.css +11 -0
  22. data/lib/pocketbook/styles/core/02_pages.css +72 -0
  23. data/lib/pocketbook/styles/core/03_layout.css +162 -0
  24. data/lib/pocketbook/styles/core/04_toc.css +62 -0
  25. data/lib/pocketbook/styles/core/05_content.css +49 -0
  26. data/lib/pocketbook/styles/core/06_running.css +48 -0
  27. data/lib/pocketbook/styles/core/07_print.css +12 -0
  28. data/lib/pocketbook/theme/manifest.rb +244 -0
  29. data/lib/pocketbook/theme.rb +268 -0
  30. data/lib/pocketbook/version.rb +3 -0
  31. data/lib/pocketbook.rb +29 -0
  32. data/themes/basic/styles/plain.css +63 -0
  33. data/themes/basic/template.html.erb +30 -0
  34. data/themes/basic/theme.yml +8 -0
  35. data/themes/classic/styles/base.css +250 -0
  36. data/themes/classic/styles/dark.css +19 -0
  37. data/themes/classic/styles/light.css +12 -0
  38. data/themes/classic/styles/sepia.css +17 -0
  39. data/themes/classic/template.html.erb +72 -0
  40. data/themes/classic/theme.yml +17 -0
  41. metadata +136 -0
@@ -0,0 +1,275 @@
1
+ require "listen"
2
+ require "set"
3
+ require "uri"
4
+ require "rbconfig"
5
+ require_relative "../book"
6
+ require_relative "../core_stylesheet"
7
+ require_relative "../theme"
8
+
9
+ module Pocketbook
10
+ module CLI
11
+ class Watcher
12
+ WATCHABLE_EXTENSIONS = %w[.md .css .erb .yml .yaml].freeze
13
+ STOP_POLL_INTERVAL = 0.1
14
+
15
+ class ChangeBuffer
16
+ def initialize(debounce_seconds:, poll_interval:, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
17
+ @debounce_seconds = debounce_seconds
18
+ @poll_interval = poll_interval
19
+ @clock = clock
20
+ @pending_paths = Set.new
21
+ @stopped = false
22
+ @last_change_at = nil
23
+ @mutex = Mutex.new
24
+ @condition = ConditionVariable.new
25
+ end
26
+
27
+ def add(paths)
28
+ normalized = paths.map { |path| File.expand_path(path) }.uniq
29
+ return if normalized.empty?
30
+
31
+ @mutex.synchronize do
32
+ normalized.each { |path| @pending_paths << path }
33
+ @last_change_at = @clock.call
34
+ @condition.broadcast
35
+ end
36
+ end
37
+
38
+ def next_batch
39
+ @mutex.synchronize do
40
+ while !@stopped && @pending_paths.empty?
41
+ @condition.wait(@mutex, @poll_interval)
42
+ end
43
+
44
+ return nil if @stopped
45
+
46
+ loop do
47
+ break if @stopped
48
+
49
+ elapsed = @clock.call - @last_change_at
50
+ remaining = @debounce_seconds - elapsed
51
+ break if remaining <= 0
52
+
53
+ @condition.wait(@mutex, [remaining, @poll_interval].min)
54
+ end
55
+
56
+ return nil if @stopped
57
+
58
+ paths = @pending_paths.to_a.sort
59
+ @pending_paths.clear
60
+ paths
61
+ end
62
+ end
63
+
64
+ def stop!
65
+ @mutex.synchronize do
66
+ return if @stopped
67
+
68
+ @stopped = true
69
+ @condition.broadcast
70
+ end
71
+ end
72
+
73
+ def wait_until_stopped
74
+ @mutex.synchronize do
75
+ @condition.wait(@mutex, @poll_interval) until @stopped
76
+ end
77
+ end
78
+ end
79
+
80
+ def initialize(request:, stdout: $stdout, stderr: $stderr)
81
+ @request = request
82
+ @stdout = stdout
83
+ @stderr = stderr
84
+ @changes = ChangeBuffer.new(
85
+ debounce_seconds: @request.debounce_ms.to_f / 1000.0,
86
+ poll_interval: STOP_POLL_INTERVAL
87
+ )
88
+ @theme_root = nil
89
+ @watch_directories = []
90
+ end
91
+
92
+ def run
93
+ theme = build_theme
94
+ @theme_root = theme.root_path
95
+ @watch_directories = compute_watch_directories
96
+
97
+ print_watch_banner
98
+ render(reason: "initial build", preloaded_theme: theme, open_output: @request.open_output)
99
+
100
+ listener = ::Listen.to(*@watch_directories) do |modified, added, removed|
101
+ register_changes(modified + added + removed)
102
+ end
103
+
104
+ worker = Thread.new { worker_loop }
105
+
106
+ with_signal_traps do
107
+ listener.start
108
+ @changes.wait_until_stopped
109
+ end
110
+ ensure
111
+ @changes.stop!
112
+ listener&.stop
113
+ worker&.join
114
+ @stdout.puts("[pocketbook] Watch stopped.")
115
+ end
116
+
117
+ private
118
+
119
+ def render(reason:, preloaded_theme: nil, open_output: false)
120
+ start_time = monotonic_time
121
+ output_path = build_book(theme: preloaded_theme).render_to(@request.output_path)
122
+ duration = monotonic_time - start_time
123
+ absolute_output = File.expand_path(output_path)
124
+
125
+ @stdout.puts("[pocketbook] Build succeeded in #{format('%.2f', duration)}s (#{reason}).")
126
+ @stdout.puts("[pocketbook] PDF: #{file_uri(absolute_output)}")
127
+ @stdout.puts("[pocketbook] Open: #{open_command(absolute_output)}")
128
+ if open_output
129
+ if open_with_default_viewer(absolute_output)
130
+ @stdout.puts("[pocketbook] Opened in default viewer.")
131
+ else
132
+ @stderr.puts("[pocketbook] Warning: could not open PDF automatically.")
133
+ end
134
+ end
135
+ rescue StandardError => e
136
+ @stderr.puts("[pocketbook] Build failed (#{reason}): #{e.message}")
137
+ end
138
+
139
+ def build_book(theme: nil)
140
+ Book.new(
141
+ inputs: @request.inputs,
142
+ theme: theme || build_theme,
143
+ metadata: @request.metadata
144
+ )
145
+ end
146
+
147
+ def build_theme
148
+ Theme.new(
149
+ theme_path: @request.theme_path,
150
+ style: @request.style,
151
+ template_override: @request.template_override
152
+ )
153
+ end
154
+
155
+ def compute_watch_directories
156
+ dirs = Set.new
157
+ @request.inputs.each do |path|
158
+ dirs << File.dirname(File.expand_path(path))
159
+ end
160
+ dirs << @theme_root if @theme_root
161
+ CoreStylesheet.watch_directories.each { |directory| dirs << directory }
162
+
163
+ dirs.to_a.select { |dir| Dir.exist?(dir) }.sort
164
+ end
165
+
166
+ def register_changes(paths)
167
+ changed_paths = paths.map { |path| File.expand_path(path) }.uniq
168
+ changed_paths.select! { |path| watchable_path?(path) }
169
+ @changes.add(changed_paths)
170
+ end
171
+
172
+ def watchable_path?(path)
173
+ WATCHABLE_EXTENSIONS.include?(File.extname(path).downcase)
174
+ end
175
+
176
+ def worker_loop
177
+ loop do
178
+ changed_paths = @changes.next_batch
179
+ break if changed_paths.nil?
180
+
181
+ render(reason: build_reason(changed_paths))
182
+ end
183
+ end
184
+
185
+ def build_reason(changed_paths)
186
+ if changed_paths.length == 1
187
+ "change detected in #{display_path(changed_paths.first)}"
188
+ else
189
+ "#{changed_paths.length} files changed"
190
+ end
191
+ end
192
+
193
+ def display_path(path)
194
+ absolute = File.expand_path(path)
195
+ cwd = File.expand_path(Dir.pwd)
196
+ if absolute.start_with?("#{cwd}/")
197
+ absolute[(cwd.length + 1)..]
198
+ else
199
+ absolute
200
+ end
201
+ end
202
+
203
+ def with_signal_traps
204
+ previous_int = trap_signal("INT")
205
+ previous_term = trap_signal("TERM")
206
+
207
+ yield
208
+ ensure
209
+ restore_signal("INT", previous_int)
210
+ restore_signal("TERM", previous_term)
211
+ end
212
+
213
+ def trap_signal(signal_name)
214
+ Signal.trap(signal_name) { request_stop_from_signal }
215
+ rescue ArgumentError
216
+ nil
217
+ end
218
+
219
+ def restore_signal(signal_name, previous_handler)
220
+ return if previous_handler.nil?
221
+
222
+ Signal.trap(signal_name, previous_handler)
223
+ rescue ArgumentError
224
+ nil
225
+ end
226
+
227
+ def request_stop_from_signal
228
+ @changes.stop!
229
+ end
230
+
231
+ def file_uri(path)
232
+ "file://#{URI::DEFAULT_PARSER.escape(path)}"
233
+ end
234
+
235
+ def open_with_default_viewer(path)
236
+ host_os = RbConfig::CONFIG["host_os"]
237
+
238
+ if host_os =~ /darwin/
239
+ system("open", path)
240
+ elsif host_os =~ /mswin|mingw|cygwin/
241
+ system("cmd", "/c", "start", "", path)
242
+ else
243
+ system("xdg-open", path)
244
+ end
245
+ end
246
+
247
+ def open_command(path)
248
+ host_os = RbConfig::CONFIG["host_os"]
249
+ if host_os =~ /darwin/
250
+ "open \"#{path}\""
251
+ elsif host_os =~ /mswin|mingw|cygwin/
252
+ "cmd /c start \"\" \"#{path}\""
253
+ else
254
+ "xdg-open \"#{path}\""
255
+ end
256
+ end
257
+
258
+ def monotonic_time
259
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
260
+ end
261
+
262
+ def print_watch_banner
263
+ @stdout.puts("[pocketbook] Watch mode enabled.")
264
+ @stdout.puts("[pocketbook] Debounce window: #{@request.debounce_ms}ms")
265
+ @stdout.puts("[pocketbook] Watching #{@watch_directories.length} directories:")
266
+ @watch_directories.each do |directory|
267
+ @stdout.puts("[pocketbook] - #{display_path(directory)}")
268
+ end
269
+ @stdout.puts("[pocketbook] Press Ctrl+C to stop.")
270
+ end
271
+ end
272
+
273
+ WatchCommand = Watcher
274
+ end
275
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "cli/options_parser"
2
+ require_relative "cli/runner"
3
+
4
+ module Pocketbook
5
+ module CLI
6
+ module_function
7
+
8
+ def run(argv = ARGV, stdout: $stdout, stderr: $stderr)
9
+ Runner.new(stdout: stdout, stderr: stderr).run(argv)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module Pocketbook
2
+ class CoreStylesheet
3
+ CORE_DIRECTORY = File.expand_path("styles/core", __dir__)
4
+
5
+ def self.load
6
+ paths = css_paths
7
+ raise ArgumentError, "No core stylesheets found in #{CORE_DIRECTORY}" if paths.empty?
8
+
9
+ paths.map { |path| File.read(path) }.join("\n\n")
10
+ end
11
+
12
+ def self.watch_directories
13
+ css_paths.map { |path| File.dirname(path) }.uniq
14
+ end
15
+
16
+ def self.css_paths
17
+ Dir.glob(File.join(CORE_DIRECTORY, "*.css")).sort
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,96 @@
1
+ require "hexapdf"
2
+
3
+ module Pocketbook
4
+ class PdfDocument
5
+ def self.open(path)
6
+ new(document: HexaPDF::Document.open(path))
7
+ end
8
+
9
+ def initialize(document:)
10
+ @document = document
11
+ end
12
+
13
+ def page_count
14
+ @document.pages.count
15
+ end
16
+
17
+ def toc_page_numbers(anchor_ids)
18
+ return {} if anchor_ids.empty?
19
+
20
+ page_number_by_reference = extract_page_numbers
21
+ destination_reference_by_id = named_destinations
22
+
23
+ anchor_ids.each_with_object({}) do |anchor_id, output|
24
+ page_reference = destination_reference_by_id[anchor_id]
25
+ next if page_reference.nil?
26
+
27
+ page_number = page_number_by_reference[page_reference]
28
+ next if page_number.nil?
29
+
30
+ output[anchor_id] = page_number
31
+ end
32
+ end
33
+
34
+ def named_destinations
35
+ {}.tap do |output|
36
+ extract_old_style_destinations(output)
37
+ extract_name_tree_destinations(output)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def extract_page_numbers
44
+ @document.pages.each_with_index.each_with_object({}) do |(page, index), output|
45
+ output[[page.oid, page.gen]] = index + 1
46
+ end
47
+ end
48
+
49
+ def extract_old_style_destinations(output)
50
+ return unless @document.catalog.key?(:Dests)
51
+
52
+ @document.catalog[:Dests].each do |target, destination|
53
+ target_id = normalize_destination_id(target)
54
+ page_reference = destination_page_reference(destination)
55
+ next if target_id.nil? || page_reference.nil?
56
+
57
+ output[target_id] ||= page_reference
58
+ end
59
+ end
60
+
61
+ def extract_name_tree_destinations(output)
62
+ names = @document.catalog[:Names]
63
+ return if names.nil? || !names.key?(:Dests)
64
+
65
+ names[:Dests].each_entry do |target, destination|
66
+ target_id = normalize_destination_id(target)
67
+ page_reference = destination_page_reference(destination)
68
+ next if target_id.nil? || page_reference.nil?
69
+
70
+ output[target_id] ||= page_reference
71
+ end
72
+ end
73
+
74
+ def normalize_destination_id(target)
75
+ return target.to_s if target.is_a?(Symbol)
76
+ return target if target.is_a?(String)
77
+
78
+ nil
79
+ end
80
+
81
+ def destination_page_reference(destination)
82
+ value = if destination.respond_to?(:key?) && destination.key?(:D)
83
+ destination[:D]
84
+ else
85
+ destination
86
+ end
87
+
88
+ return nil unless value.is_a?(Array)
89
+
90
+ page = value.first
91
+ return nil unless page.respond_to?(:oid) && page.respond_to?(:gen)
92
+
93
+ [page.oid, page.gen]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,67 @@
1
+ module Pocketbook
2
+ class RenderRequest
3
+ attr_reader :inputs, :theme_path, :style, :template_override, :output_path, :metadata, :debounce_ms, :diagnostics, :open_output
4
+
5
+ def initialize(
6
+ inputs:,
7
+ theme_path:,
8
+ style: nil,
9
+ template_override: nil,
10
+ output_path:,
11
+ metadata: {},
12
+ debounce_ms: 350,
13
+ diagnostics: false,
14
+ open_output: false
15
+ )
16
+ @inputs = normalize_inputs(inputs)
17
+ @theme_path = normalize_required_string(theme_path, "theme_path")
18
+ @style = normalize_optional_string(style)
19
+ @template_override = normalize_optional_string(template_override)
20
+ @output_path = normalize_required_string(output_path, "output_path")
21
+ @metadata = symbolize_keys(metadata || {}).freeze
22
+ @debounce_ms = normalize_debounce_ms(debounce_ms)
23
+ @diagnostics = !!diagnostics
24
+ @open_output = !!open_output
25
+
26
+ freeze
27
+ end
28
+
29
+ private
30
+
31
+ def normalize_inputs(values)
32
+ items = Array(values).map { |value| normalize_required_string(value, "inputs") }
33
+ raise ArgumentError, "At least one markdown input file is required" if items.empty?
34
+
35
+ items.freeze
36
+ end
37
+
38
+ def normalize_required_string(value, label)
39
+ normalized = value.to_s.strip
40
+ raise ArgumentError, "#{label} is required" if normalized.empty?
41
+
42
+ normalized
43
+ end
44
+
45
+ def normalize_optional_string(value)
46
+ return nil if value.nil?
47
+
48
+ normalized = value.to_s.strip
49
+ return nil if normalized.empty?
50
+
51
+ normalized
52
+ end
53
+
54
+ def normalize_debounce_ms(value)
55
+ normalized = Integer(value)
56
+ raise ArgumentError, "debounce_ms must be zero or positive" if normalized.negative?
57
+
58
+ normalized
59
+ end
60
+
61
+ def symbolize_keys(hash)
62
+ hash.each_with_object({}) do |(key, value), output|
63
+ output[key.to_sym] = value
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ :root {
2
+ --book-size: 6in 9in;
3
+ --paper: #ffffff;
4
+ --text-color: #161616;
5
+ --folio-color: #5f5f5f;
6
+ --cover-bg: #151515;
7
+ --cover-fg: #f5f5f5;
8
+ --accent: #b88539;
9
+ --font-body: "Iowan Old Style", "Palatino Linotype", serif;
10
+ --font-display: "Avenir Next", "Helvetica Neue", sans-serif;
11
+ }
@@ -0,0 +1,72 @@
1
+ @page {
2
+ size: var(--book-size);
3
+ background: var(--paper);
4
+ margin-top: 18mm;
5
+ margin-right: 16mm;
6
+ margin-bottom: 22mm;
7
+ margin-left: 16mm;
8
+ }
9
+
10
+ @page :left {
11
+ background: var(--paper);
12
+ margin-left: 20mm;
13
+ margin-right: 14mm;
14
+
15
+ @top-left {
16
+ content: var(--book-title-label);
17
+ font-family: var(--font-display);
18
+ font-size: 8.5pt;
19
+ letter-spacing: 0.04em;
20
+ color: var(--folio-color);
21
+ }
22
+
23
+ @bottom-left {
24
+ content: var(--publisher-label);
25
+ font-family: var(--font-display);
26
+ font-size: 8.5pt;
27
+ color: var(--folio-color);
28
+ }
29
+
30
+ @bottom-right {
31
+ content: counter(page);
32
+ font-family: var(--font-display);
33
+ font-size: 8.5pt;
34
+ color: var(--folio-color);
35
+ }
36
+ }
37
+
38
+ @page :right {
39
+ background: var(--paper);
40
+ margin-left: 14mm;
41
+ margin-right: 20mm;
42
+
43
+ @top-right {
44
+ content: var(--book-title-label);
45
+ font-family: var(--font-display);
46
+ font-size: 8.5pt;
47
+ letter-spacing: 0.04em;
48
+ color: var(--folio-color);
49
+ }
50
+
51
+ @bottom-left {
52
+ content: var(--publisher-label);
53
+ font-family: var(--font-display);
54
+ font-size: 8.5pt;
55
+ color: var(--folio-color);
56
+ }
57
+
58
+ @bottom-right {
59
+ content: counter(page);
60
+ font-family: var(--font-display);
61
+ font-size: 8.5pt;
62
+ color: var(--folio-color);
63
+ }
64
+ }
65
+
66
+ @page cover {
67
+ margin: 0;
68
+ }
69
+
70
+ @page backcover {
71
+ margin: 0;
72
+ }
@@ -0,0 +1,162 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ html,
6
+ body {
7
+ margin: 0;
8
+ padding: 0;
9
+ color: var(--text-color);
10
+ background: var(--paper);
11
+ font-family: var(--font-body);
12
+ font-size: 11pt;
13
+ line-height: 1.55;
14
+ }
15
+
16
+ .cover,
17
+ .backcover {
18
+ min-height: 100vh;
19
+ display: grid;
20
+ position: relative;
21
+ z-index: 30;
22
+ }
23
+
24
+ .cover {
25
+ page: cover;
26
+ break-after: page;
27
+ color: var(--cover-fg);
28
+ background: var(--cover-bg);
29
+ }
30
+
31
+ .backcover {
32
+ page: backcover;
33
+ break-before: page;
34
+ color: var(--cover-fg);
35
+ background: #222;
36
+ }
37
+
38
+ .cover-inner,
39
+ .backcover-inner {
40
+ align-self: stretch;
41
+ display: grid;
42
+ gap: 0.8rem;
43
+ padding: 16mm;
44
+ align-content: center;
45
+ }
46
+
47
+ .cover-title {
48
+ margin: 0;
49
+ font-family: var(--font-display);
50
+ font-size: 2.1rem;
51
+ line-height: 1.1;
52
+ letter-spacing: 0.02em;
53
+ }
54
+
55
+ .cover-subtitle {
56
+ margin: 0;
57
+ opacity: 0.9;
58
+ font-size: 1rem;
59
+ }
60
+
61
+ .cover-author {
62
+ margin-top: 1rem;
63
+ margin-bottom: 0;
64
+ font-family: var(--font-display);
65
+ font-size: 0.95rem;
66
+ letter-spacing: 0.05em;
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ .backcover-title {
71
+ margin: 0;
72
+ font-family: var(--font-display);
73
+ color: var(--accent);
74
+ letter-spacing: 0.04em;
75
+ text-transform: uppercase;
76
+ font-size: 0.8rem;
77
+ }
78
+
79
+ .backcover-copy {
80
+ margin: 0;
81
+ font-size: 0.95rem;
82
+ opacity: 0.95;
83
+ }
84
+
85
+ .toc {
86
+ break-before: page;
87
+ break-after: page;
88
+ }
89
+
90
+ .toc,
91
+ .chapter {
92
+ padding: 0;
93
+ }
94
+
95
+ @media screen {
96
+ html,
97
+ body {
98
+ min-height: 100%;
99
+ }
100
+
101
+ html {
102
+ scroll-snap-type: y mandatory;
103
+ }
104
+
105
+ .cover,
106
+ .toc,
107
+ .book-content,
108
+ .chapter,
109
+ .backcover,
110
+ [data-pocketbook-page-spacer],
111
+ [data-pocketbook-flow-spacer] {
112
+ scroll-snap-align: start;
113
+ }
114
+
115
+ [data-pocketbook-page-spacer],
116
+ [data-pocketbook-flow-spacer] {
117
+ scroll-snap-stop: always;
118
+ }
119
+
120
+ :root {
121
+ --pocketbook-screen-left-margin: 16mm;
122
+ --pocketbook-screen-right-margin: 16mm;
123
+ }
124
+
125
+ :root[data-pocketbook-page-parity="left"] {
126
+ --pocketbook-screen-left-margin: 20mm;
127
+ --pocketbook-screen-right-margin: 14mm;
128
+ }
129
+
130
+ :root[data-pocketbook-page-parity="right"] {
131
+ --pocketbook-screen-left-margin: 14mm;
132
+ --pocketbook-screen-right-margin: 20mm;
133
+ }
134
+
135
+ .toc,
136
+ .chapter {
137
+ /* Screen preview uses fixed running header/footer overlays. */
138
+ padding-top: 24mm;
139
+ padding-right: var(--pocketbook-screen-right-margin);
140
+ padding-bottom: 28mm;
141
+ padding-left: var(--pocketbook-screen-left-margin);
142
+ min-height: 100vh;
143
+ }
144
+
145
+ .running-header,
146
+ .running-footer {
147
+ padding-left: var(--pocketbook-screen-left-margin);
148
+ padding-right: var(--pocketbook-screen-right-margin);
149
+ }
150
+
151
+ :root[data-pocketbook-page-parity="left"] .running-header {
152
+ justify-content: flex-start;
153
+ }
154
+
155
+ :root[data-pocketbook-page-parity="right"] .running-header {
156
+ justify-content: flex-end;
157
+ }
158
+
159
+ .running-header-chapter {
160
+ display: none;
161
+ }
162
+ }