Deploy 7977791 from GitHub
Browse files- R/config.R +6 -4
- R/help.R +20 -1
- R/ui.R +27 -13
- README.md +3 -1
- www/app.js +12 -8
- www/theme.css +23 -3
R/config.R
CHANGED
|
@@ -4,8 +4,8 @@
|
|
| 4 |
# behaviour.
|
| 5 |
|
| 6 |
# Version of this app
|
| 7 |
-
APP_VERSION <- "1.0.
|
| 8 |
-
APP_DATE <- "
|
| 9 |
|
| 10 |
# Model used for every call. This is the ONLY place a model id may be written
|
| 11 |
# (tests/testthat/test-config.R enforces this).
|
|
@@ -60,9 +60,11 @@ USAGE_TOKEN_VAR <- "HF_USAGE_TOKEN"
|
|
| 60 |
USAGE_REPO <- "fmegahed/decisions-space-usage"
|
| 61 |
STATUS_URL <- "https://raw.githubusercontent.com/fmegahed/decisions-space/main/status/status.csv"
|
| 62 |
|
| 63 |
-
# Address of the Space, and the video on YouTube ("" hides
|
|
|
|
| 64 |
SPACE_HOST <- "https://fmegahed-decisions.hf.space"
|
| 65 |
VIDEO_URL <- "https://youtu.be/4WE-ykXzK1g"
|
|
|
|
| 66 |
|
| 67 |
# The values that the help text uses, in one named list
|
| 68 |
app_config <- function() {
|
|
@@ -77,6 +79,6 @@ app_config <- function() {
|
|
| 77 |
max_description = MAX_DESCRIPTION, answer_range = ANSWER_RANGE,
|
| 78 |
max_references = MAX_REFERENCES, chunk_size = CHUNK_SIZE,
|
| 79 |
image_settings = IMAGE_SETTINGS, space_host = SPACE_HOST,
|
| 80 |
-
video_url =
|
| 81 |
)
|
| 82 |
}
|
|
|
|
| 4 |
# behaviour.
|
| 5 |
|
| 6 |
# Version of this app
|
| 7 |
+
APP_VERSION <- "1.0.1"
|
| 8 |
+
APP_DATE <- "30 October 2026"
|
| 9 |
|
| 10 |
# Model used for every call. This is the ONLY place a model id may be written
|
| 11 |
# (tests/testthat/test-config.R enforces this).
|
|
|
|
| 60 |
USAGE_REPO <- "fmegahed/decisions-space-usage"
|
| 61 |
STATUS_URL <- "https://raw.githubusercontent.com/fmegahed/decisions-space/main/status/status.csv"
|
| 62 |
|
| 63 |
+
# Address of the Space, and the help video on YouTube ("" hides its buttons)
|
| 64 |
+
# with its length in whole minutes (the video runs 4:59)
|
| 65 |
SPACE_HOST <- "https://fmegahed-decisions.hf.space"
|
| 66 |
VIDEO_URL <- "https://youtu.be/4WE-ykXzK1g"
|
| 67 |
+
VIDEO_MINUTES <- 5
|
| 68 |
|
| 69 |
# The values that the help text uses, in one named list
|
| 70 |
app_config <- function() {
|
|
|
|
| 79 |
max_description = MAX_DESCRIPTION, answer_range = ANSWER_RANGE,
|
| 80 |
max_references = MAX_REFERENCES, chunk_size = CHUNK_SIZE,
|
| 81 |
image_settings = IMAGE_SETTINGS, space_host = SPACE_HOST,
|
| 82 |
+
video_url = video_url(), video_minutes = VIDEO_MINUTES
|
| 83 |
)
|
| 84 |
}
|
R/help.R
CHANGED
|
@@ -56,7 +56,10 @@ help_text <- function(key) {
|
|
| 56 |
)
|
| 57 |
}
|
| 58 |
|
| 59 |
-
# The text of the About tab, as Markdown
|
|
|
|
|
|
|
|
|
|
| 60 |
about_text <- function(cfg = app_config()) {
|
| 61 |
usd <- \(x) paste("USD", format(x, big.mark = ","))
|
| 62 |
num <- \(x) format(x, big.mark = ",")
|
|
@@ -65,6 +68,19 @@ about_text <- function(cfg = app_config()) {
|
|
| 65 |
"",
|
| 66 |
"This app asks Jev, a decision model of TypeSafe, a question about each image or text in a set. For each input, it shows the answer and the probability of each answer. It also gives the R code of the run and a bundle with the setup and the answers. You need no key and no account.",
|
| 67 |
"",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"## Package",
|
| 69 |
"",
|
| 70 |
sprintf(
|
|
@@ -115,6 +131,9 @@ about_text <- function(cfg = app_config()) {
|
|
| 115 |
cfg$version, cfg$date, decisions_version(), cfg$model
|
| 116 |
),
|
| 117 |
"",
|
|
|
|
|
|
|
|
|
|
| 118 |
"## Cite",
|
| 119 |
"",
|
| 120 |
sprintf(
|
|
|
|
| 56 |
)
|
| 57 |
}
|
| 58 |
|
| 59 |
+
# The text of the About tab, as Markdown. HELP_VIDEO_MARK marks the place of
|
| 60 |
+
# the button that opens the help video (about_panel() puts it there).
|
| 61 |
+
HELP_VIDEO_MARK <- "<!-- help-video-button -->"
|
| 62 |
+
|
| 63 |
about_text <- function(cfg = app_config()) {
|
| 64 |
usd <- \(x) paste("USD", format(x, big.mark = ","))
|
| 65 |
num <- \(x) format(x, big.mark = ",")
|
|
|
|
| 68 |
"",
|
| 69 |
"This app asks Jev, a decision model of TypeSafe, a question about each image or text in a set. For each input, it shows the answer and the probability of each answer. It also gives the R code of the run and a bundle with the setup and the answers. You need no key and no account.",
|
| 70 |
"",
|
| 71 |
+
if (nzchar(cfg$video_url)) {
|
| 72 |
+
c(
|
| 73 |
+
"## Help video",
|
| 74 |
+
"",
|
| 75 |
+
sprintf(
|
| 76 |
+
"A %d-minute video shows how to use the app: a sample of images, your own images, a text sample and your own texts. It explains the three question types (Yes / no, Choice and Score), the answers and the R code. Watch it here, or [on YouTube](%s) with chapters.",
|
| 77 |
+
cfg$video_minutes, cfg$video_url
|
| 78 |
+
),
|
| 79 |
+
"",
|
| 80 |
+
HELP_VIDEO_MARK,
|
| 81 |
+
""
|
| 82 |
+
)
|
| 83 |
+
},
|
| 84 |
"## Package",
|
| 85 |
"",
|
| 86 |
sprintf(
|
|
|
|
| 131 |
cfg$version, cfg$date, decisions_version(), cfg$model
|
| 132 |
),
|
| 133 |
"",
|
| 134 |
+
"- 1.0.1: the help video (in the intro, the top bar and this page); the buttons of the top bar on one line.",
|
| 135 |
+
"- 1.0.0: the first public version.",
|
| 136 |
+
"",
|
| 137 |
"## Cite",
|
| 138 |
"",
|
| 139 |
sprintf(
|
R/ui.R
CHANGED
|
@@ -125,17 +125,18 @@ top_bar <- function(video) {
|
|
| 125 |
class = "chip", shiny::span(class = "dot", `aria-hidden` = "true", "\u25cf"), " ",
|
| 126 |
shiny::textOutput("status_chip", inline = TRUE)
|
| 127 |
),
|
| 128 |
-
if (nzchar(video)) {
|
| 129 |
-
shiny::tags$button(
|
| 130 |
-
id = "open_video", type = "button", class = "btn btn-quiet",
|
| 131 |
-
`aria-haspopup` = "dialog", "Video"
|
| 132 |
-
)
|
| 133 |
-
},
|
| 134 |
shiny::div(
|
| 135 |
class = "import-btn",
|
| 136 |
upload_input("import_file", NULL, c(".zip", ".json"), button = "Import")
|
| 137 |
),
|
| 138 |
-
samples_menu()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
)
|
| 140 |
)
|
| 141 |
}
|
|
@@ -205,7 +206,17 @@ sidebar <- function() {
|
|
| 205 |
|
| 206 |
# --- Build tab: cards -------------------------------------------------------
|
| 207 |
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
step <- \(n, name, text) {
|
| 210 |
shiny::tags$li(
|
| 211 |
shiny::span(class = "num", `aria-hidden` = "true", n),
|
|
@@ -216,7 +227,8 @@ intro_card <- function() {
|
|
| 216 |
id = "intro", class = "card intro", `aria-label` = "Intro",
|
| 217 |
shiny::div(
|
| 218 |
shiny::p(class = "intro-lead", "Ask a model a question about each image or text."),
|
| 219 |
-
shiny::p(class = "intro-help", help_text("intro"), " ", shiny::actionLink("intro_hide", "Hide"))
|
|
|
|
| 220 |
),
|
| 221 |
shiny::tags$ol(
|
| 222 |
class = "steps",
|
|
@@ -318,10 +330,12 @@ build_panel <- function() {
|
|
| 318 |
# --- About and Uptime tabs --------------------------------------------------
|
| 319 |
|
| 320 |
about_panel <- function() {
|
| 321 |
-
# The About text is Markdown; its links open in a new tab
|
|
|
|
| 322 |
html <- shiny::markdown(help_text("about")) |>
|
| 323 |
as.character() |>
|
| 324 |
-
stringr::str_replace_all("<a href=", '<a target="_blank" rel="noopener" href=')
|
|
|
|
| 325 |
shiny::div(class = "page card", shiny::div(class = "bd prose", htmltools::HTML(html)))
|
| 326 |
}
|
| 327 |
|
|
@@ -371,11 +385,11 @@ video_dialog <- function(url) {
|
|
| 371 |
id = "video_dialog", class = "video-dialog", `aria-labelledby` = "video_title",
|
| 372 |
shiny::div(
|
| 373 |
class = "video-head",
|
| 374 |
-
shiny::h2(id = "video_title", "
|
| 375 |
shiny::tags$button(id = "close_video", type = "button", class = "btn btn-quiet sm", "Close")
|
| 376 |
),
|
| 377 |
shiny::tags$iframe(
|
| 378 |
-
id = "video_frame", class = "video-frame", title = "
|
| 379 |
`data-src` = video_embed(url), allow = "autoplay; encrypted-media; picture-in-picture; fullscreen",
|
| 380 |
referrerpolicy = "strict-origin-when-cross-origin"
|
| 381 |
)
|
|
|
|
| 125 |
class = "chip", shiny::span(class = "dot", `aria-hidden` = "true", "\u25cf"), " ",
|
| 126 |
shiny::textOutput("status_chip", inline = TRUE)
|
| 127 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
shiny::div(
|
| 129 |
class = "import-btn",
|
| 130 |
upload_input("import_file", NULL, c(".zip", ".json"), button = "Import")
|
| 131 |
),
|
| 132 |
+
samples_menu(),
|
| 133 |
+
# The quiet way to the help video: on every tab, and after Hide
|
| 134 |
+
if (nzchar(video)) {
|
| 135 |
+
shiny::tags$button(
|
| 136 |
+
id = "open_video_bar", type = "button", class = "btn btn-quiet help-video-bar open-video",
|
| 137 |
+
`aria-haspopup` = "dialog", shiny::span(class = "tri", `aria-hidden` = "true"), "Help video"
|
| 138 |
+
)
|
| 139 |
+
}
|
| 140 |
)
|
| 141 |
)
|
| 142 |
}
|
|
|
|
| 206 |
|
| 207 |
# --- Build tab: cards -------------------------------------------------------
|
| 208 |
|
| 209 |
+
# The yellow button that opens the help video (in the intro and in About)
|
| 210 |
+
help_video_button <- function(id) {
|
| 211 |
+
shiny::tags$button(
|
| 212 |
+
id = id, type = "button", class = "btn-help-video open-video", `aria-haspopup` = "dialog",
|
| 213 |
+
shiny::span(class = "play", `aria-hidden` = "true"),
|
| 214 |
+
sprintf("Watch our %d-minute intro video on how to use this app", VIDEO_MINUTES)
|
| 215 |
+
)
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
# The intro, with the help video as the first step of a first visit
|
| 219 |
+
intro_card <- function(video = video_url()) {
|
| 220 |
step <- \(n, name, text) {
|
| 221 |
shiny::tags$li(
|
| 222 |
shiny::span(class = "num", `aria-hidden` = "true", n),
|
|
|
|
| 227 |
id = "intro", class = "card intro", `aria-label` = "Intro",
|
| 228 |
shiny::div(
|
| 229 |
shiny::p(class = "intro-lead", "Ask a model a question about each image or text."),
|
| 230 |
+
shiny::p(class = "intro-help", help_text("intro"), " ", shiny::actionLink("intro_hide", "Hide")),
|
| 231 |
+
if (nzchar(video)) shiny::div(class = "help-video-row", help_video_button("open_video"))
|
| 232 |
),
|
| 233 |
shiny::tags$ol(
|
| 234 |
class = "steps",
|
|
|
|
| 330 |
# --- About and Uptime tabs --------------------------------------------------
|
| 331 |
|
| 332 |
about_panel <- function() {
|
| 333 |
+
# The About text is Markdown; its links open in a new tab. The button of
|
| 334 |
+
# the help video goes at its mark.
|
| 335 |
html <- shiny::markdown(help_text("about")) |>
|
| 336 |
as.character() |>
|
| 337 |
+
stringr::str_replace_all("<a href=", '<a target="_blank" rel="noopener" href=') |>
|
| 338 |
+
stringr::str_replace(stringr::fixed(HELP_VIDEO_MARK), as.character(help_video_button("open_video_about")))
|
| 339 |
shiny::div(class = "page card", shiny::div(class = "bd prose", htmltools::HTML(html)))
|
| 340 |
}
|
| 341 |
|
|
|
|
| 385 |
id = "video_dialog", class = "video-dialog", `aria-labelledby` = "video_title",
|
| 386 |
shiny::div(
|
| 387 |
class = "video-head",
|
| 388 |
+
shiny::h2(id = "video_title", "Help video"),
|
| 389 |
shiny::tags$button(id = "close_video", type = "button", class = "btn btn-quiet sm", "Close")
|
| 390 |
),
|
| 391 |
shiny::tags$iframe(
|
| 392 |
+
id = "video_frame", class = "video-frame", title = "Help video: how to use the decisions app",
|
| 393 |
`data-src` = video_embed(url), allow = "autoplay; encrypted-media; picture-in-picture; fullscreen",
|
| 394 |
referrerpolicy = "strict-origin-when-cross-origin"
|
| 395 |
)
|
README.md
CHANGED
|
@@ -20,6 +20,8 @@ The app uses the R package [decisions](https://github.com/fmegahed/decisions) (v
|
|
| 20 |
|
| 21 |
Open the app: <https://huggingface.co/spaces/fmegahed/decisions>
|
| 22 |
|
|
|
|
|
|
|
| 23 |
## Privacy
|
| 24 |
|
| 25 |
Your images, texts and references go to TypeSafe (Jev). Do not upload confidential data. The app keeps no input after your session. It stores only the daily totals of calls, tokens and cost. The bundle and the logs hold no key.
|
|
@@ -87,4 +89,4 @@ Fadel M. Megahed (Miami University) and Ying-Ju (Tessa) Chen (University of Dayt
|
|
| 87 |
If you use the package or the app, cite both:
|
| 88 |
|
| 89 |
- Megahed, F. M., & Chen, Y.-J. (2026). *decisions: Typed decisions from decision models* (R package version 0.1.0). https://github.com/fmegahed/decisions
|
| 90 |
-
- Megahed, F. M., & Chen, Y.-J. (2026). *decisions Space* (Version 1.0.
|
|
|
|
| 20 |
|
| 21 |
Open the app: <https://huggingface.co/spaces/fmegahed/decisions>
|
| 22 |
|
| 23 |
+
Help video (5 minutes, with chapters): <https://youtu.be/4WE-ykXzK1g>
|
| 24 |
+
|
| 25 |
## Privacy
|
| 26 |
|
| 27 |
Your images, texts and references go to TypeSafe (Jev). Do not upload confidential data. The app keeps no input after your session. It stores only the daily totals of calls, tokens and cost. The bundle and the logs hold no key.
|
|
|
|
| 89 |
If you use the package or the app, cite both:
|
| 90 |
|
| 91 |
- Megahed, F. M., & Chen, Y.-J. (2026). *decisions: Typed decisions from decision models* (R package version 0.1.0). https://github.com/fmegahed/decisions
|
| 92 |
+
- Megahed, F. M., & Chen, Y.-J. (2026). *decisions Space* (Version 1.0.1) [Web application]. Hugging Face. https://huggingface.co/spaces/fmegahed/decisions
|
www/app.js
CHANGED
|
@@ -103,22 +103,26 @@
|
|
| 103 |
});
|
| 104 |
|
| 105 |
// ---- Video dialog (only when the page has one) ---------------------------
|
|
|
|
| 106 |
// The embed address is set when the dialog opens and cleared when it
|
| 107 |
// closes, so nothing loads before a click and the video stops on close.
|
| 108 |
-
// Escape closes the dialog; Tab stays in the dialog.
|
|
|
|
| 109 |
|
| 110 |
var dialog = byId('video_dialog');
|
| 111 |
-
var
|
| 112 |
-
if (dialog &&
|
| 113 |
var frame = byId('video_frame');
|
| 114 |
var closeButton = byId('close_video');
|
| 115 |
var lastFocused = null;
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
| 122 |
});
|
| 123 |
closeButton.addEventListener('click', function() { dialog.close(); });
|
| 124 |
dialog.addEventListener('click', function(event) {
|
|
|
|
| 103 |
});
|
| 104 |
|
| 105 |
// ---- Video dialog (only when the page has one) ---------------------------
|
| 106 |
+
// Two buttons open it: the one of the intro and the one of the top bar.
|
| 107 |
// The embed address is set when the dialog opens and cleared when it
|
| 108 |
// closes, so nothing loads before a click and the video stops on close.
|
| 109 |
+
// Escape closes the dialog; Tab stays in the dialog. Focus goes back to
|
| 110 |
+
// the button that opened it (some browsers do not focus a clicked button).
|
| 111 |
|
| 112 |
var dialog = byId('video_dialog');
|
| 113 |
+
var openers = document.querySelectorAll('.open-video');
|
| 114 |
+
if (dialog && openers.length && dialog.showModal) {
|
| 115 |
var frame = byId('video_frame');
|
| 116 |
var closeButton = byId('close_video');
|
| 117 |
var lastFocused = null;
|
| 118 |
|
| 119 |
+
Array.prototype.forEach.call(openers, function(opener) {
|
| 120 |
+
opener.addEventListener('click', function() {
|
| 121 |
+
lastFocused = opener;
|
| 122 |
+
frame.setAttribute('src', frame.getAttribute('data-src'));
|
| 123 |
+
dialog.showModal();
|
| 124 |
+
closeButton.focus();
|
| 125 |
+
});
|
| 126 |
});
|
| 127 |
closeButton.addEventListener('click', function() { dialog.close(); });
|
| 128 |
dialog.addEventListener('click', function(event) {
|
www/theme.css
CHANGED
|
@@ -23,7 +23,7 @@
|
|
| 23 |
|
| 24 |
:root {
|
| 25 |
--red: #C41230; --tan: #EDECE2; --tan2: #CCC9B8; --tan3: #70685C;
|
| 26 |
-
--warm: #FAF9F7; --slate: #3E5468; --ink: #1b1b1b; --muted: #5f5a52;
|
| 27 |
--code-bg: #fbfaf7; --code-text: #22262b; --code-c: #5f5a52; --code-f: #3E5468; --code-s: #7a3e00;
|
| 28 |
--serif: "Source Serif 4", Georgia, serif;
|
| 29 |
--sans: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
@@ -97,11 +97,20 @@ select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible, .btn:foc
|
|
| 97 |
.chip:has(.shiny-text-output:empty) { visibility: hidden; }
|
| 98 |
|
| 99 |
/* Import: a file input that looks like one button */
|
|
|
|
|
|
|
| 100 |
.import-btn .form-group { margin: 0; width: auto !important; }
|
| 101 |
.import-btn .control-label, .import-btn .form-control, .import-btn .progress { display: none; }
|
| 102 |
-
.import-btn .input-group { display:
|
| 103 |
-
.import-btn .input-group-btn { display:
|
| 104 |
.import-btn .btn-file { border-radius: 6px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
/* Samples menu */
|
| 107 |
.menu { position: relative; }
|
|
@@ -213,6 +222,17 @@ body.is-running .run-row { display: none; }
|
|
| 213 |
.num { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
|
| 214 |
border-radius: 11px; background: var(--red); color: #fff; font-weight: 700; font-size: 12px; }
|
| 215 |
@media (max-width: 899px) { .intro { grid-template-columns: minmax(0, 1fr); gap: 12px; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
.stats { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 22px; margin: 0 0 10px; }
|
| 218 |
.stats .v { font-family: var(--serif); font-size: 24px; font-weight: 700; }
|
|
|
|
| 23 |
|
| 24 |
:root {
|
| 25 |
--red: #C41230; --tan: #EDECE2; --tan2: #CCC9B8; --tan3: #70685C;
|
| 26 |
+
--warm: #FAF9F7; --slate: #3E5468; --ink: #1b1b1b; --muted: #5f5a52; --corn: #FDE778;
|
| 27 |
--code-bg: #fbfaf7; --code-text: #22262b; --code-c: #5f5a52; --code-f: #3E5468; --code-s: #7a3e00;
|
| 28 |
--serif: "Source Serif 4", Georgia, serif;
|
| 29 |
--sans: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
|
|
| 97 |
.chip:has(.shiny-text-output:empty) { visibility: hidden; }
|
| 98 |
|
| 99 |
/* Import: a file input that looks like one button */
|
| 100 |
+
/* Its wrappers are flex boxes and its label has no margin (Bootstrap gives
|
| 101 |
+
labels 5px below): the button then sits on the line of the other controls */
|
| 102 |
.import-btn .form-group { margin: 0; width: auto !important; }
|
| 103 |
.import-btn .control-label, .import-btn .form-control, .import-btn .progress { display: none; }
|
| 104 |
+
.import-btn, .import-btn .form-group, .import-btn .input-group { display: flex; align-items: center; }
|
| 105 |
+
.import-btn .input-group-btn { display: flex; width: auto; margin: 0; font-size: inherit; }
|
| 106 |
.import-btn .btn-file { border-radius: 6px !important; }
|
| 107 |
+
/* Every control of the top bar has one height */
|
| 108 |
+
.topbar-right .btn { height: 36px; box-sizing: border-box; }
|
| 109 |
+
|
| 110 |
+
/* Help video: a red play mark on the quiet button of the top bar */
|
| 111 |
+
.help-video-bar { gap: 8px; }
|
| 112 |
+
.tri { flex: none; width: 0; height: 0; border-left: 8px solid var(--red);
|
| 113 |
+
border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
|
| 114 |
|
| 115 |
/* Samples menu */
|
| 116 |
.menu { position: relative; }
|
|
|
|
| 222 |
.num { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
|
| 223 |
border-radius: 11px; background: var(--red); color: #fff; font-weight: 700; font-size: 12px; }
|
| 224 |
@media (max-width: 899px) { .intro { grid-template-columns: minmax(0, 1fr); gap: 12px; } }
|
| 225 |
+
/* The help video of the intro: Corn Yellow with ink text (about 15:1) and a
|
| 226 |
+
Dark Tan edge, so it stands apart from the red and black controls */
|
| 227 |
+
.help-video-row { margin-top: 12px; }
|
| 228 |
+
.btn-help-video { display: inline-flex; align-items: center; gap: 10px; padding: 6px 16px 6px 6px;
|
| 229 |
+
background: var(--corn); border: 1px solid var(--tan3); border-radius: 999px; color: var(--ink);
|
| 230 |
+
font-family: var(--sans); font-weight: 600; font-size: 15px; line-height: 1.25; text-align: left; cursor: pointer; }
|
| 231 |
+
.btn-help-video:hover { text-decoration: underline; }
|
| 232 |
+
.play { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px;
|
| 233 |
+
border-radius: 50%; background: var(--red); }
|
| 234 |
+
.play::before { content: ""; margin-left: 3px; border-left: 10px solid #fff;
|
| 235 |
+
border-top: 6px solid transparent; border-bottom: 6px solid transparent; }
|
| 236 |
|
| 237 |
.stats { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 22px; margin: 0 0 10px; }
|
| 238 |
.stats .v { font-family: var(--serif); font-size: 24px; font-weight: 700; }
|