From 191aa685fa5c2fe828607053a24e70395b42e85e Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 00:52:02 -0500 Subject: [PATCH 01/12] Fix shadowed category_label warning in the report-question form A local category_label in _build_report_modal() shadowed the class-level category_label field (the question's topic tag near the stem). Renamed the local to report_category_label. --- scripts/ui/question_player.gd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 0067286..86dad0d 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -2824,10 +2824,10 @@ func _build_report_modal() -> void: report_form_content.add_theme_constant_override("separation", 12) modal_body.add_child(report_form_content) - var category_label := Label.new() - category_label.text = "What is the problem?" - _set_scaled_label(category_label, 18, Style.CYAN) - report_form_content.add_child(category_label) + var report_category_label := Label.new() + report_category_label.text = "What is the problem?" + _set_scaled_label(report_category_label, 18, Style.CYAN) + report_form_content.add_child(report_category_label) var category_grid := GridContainer.new() category_grid.columns = 2 From ce4db7cec8cd782fa1a07bac8cee57d726073435 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 00:52:12 -0500 Subject: [PATCH 02/12] Remove the login panel scrollbar The auth panel was a fixed 710x820 (640x790 on narrower desktop windows) regardless of which view was showing, but Register and the Steam linking form need up to ~990px to lay out without scrolling - so the inner ScrollContainer was always kicking in for those views. - Tightened form spacing (_new_form separation 16->10, field height 68->56, panel_content separation 24->16, page margins 56->28) to shrink how much height each view actually needs. - Size the panel to the tallest view's real requirement (1000/1010), clamped to whatever height the window actually has so a short window shrinks the panel - and lets the ScrollContainer take over as a fallback - instead of the panel overflowing off-screen. Verified headlessly across 1920x1080, 1366x768, and a narrow compact-desktop width: no scrollbar in any mode at the two common resolutions, and the panel now stays within the window (falling back to scrolling only for the two tallest forms on the 768px-tall case, rather than clipping off both edges of the screen as a fixed height would have). --- scripts/ui/login_screen.gd | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 432f99f..7f0c399 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -124,8 +124,8 @@ func _build_ui() -> void: page_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) page_margin.add_theme_constant_override("margin_left", 52) page_margin.add_theme_constant_override("margin_right", 52) - page_margin.add_theme_constant_override("margin_top", 56) - page_margin.add_theme_constant_override("margin_bottom", 56) + page_margin.add_theme_constant_override("margin_top", 28) + page_margin.add_theme_constant_override("margin_bottom", 28) add_child(page_margin) var columns := HBoxContainer.new() @@ -189,7 +189,7 @@ func _build_ui() -> void: panel_center.size_flags_vertical = Control.SIZE_EXPAND_FILL columns.add_child(panel_center) auth_panel = PanelContainer.new() - auth_panel.custom_minimum_size = Vector2(710, 820) + auth_panel.custom_minimum_size = Vector2(710, 1000) panel_center.add_child(auth_panel) var scroll := ScrollContainer.new() scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED @@ -200,7 +200,7 @@ func _build_ui() -> void: scroll.add_child(panel_gutter) panel_content = VBoxContainer.new() panel_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL - panel_content.add_theme_constant_override("separation", 24) + panel_content.add_theme_constant_override("separation", 16) panel_gutter.add_child(panel_content) var panel_header := HBoxContainer.new() @@ -357,7 +357,7 @@ func _open_legal_document(kind: String) -> void: func _new_form() -> VBoxContainer: var form := VBoxContainer.new() - form.add_theme_constant_override("separation", 16) + form.add_theme_constant_override("separation", 10) return form @@ -388,7 +388,7 @@ func _add_field(parent: VBoxContainer, label_text: String, placeholder: String, input.secret = secret input.caret_blink = true input.caret_blink_interval = 0.5 - input.custom_minimum_size = Vector2(0, 68) + input.custom_minimum_size = Vector2(0, 56) input.add_theme_font_size_override("font_size", 17) input.add_theme_font_override("font", Brand.font) parent.add_child(input) @@ -487,6 +487,9 @@ func _update_responsive_layout() -> void: var layout_width := size.x if layout_width <= 0.0: layout_width = get_viewport_rect().size.x + var layout_height := size.y + if layout_height <= 0.0: + layout_height = get_viewport_rect().size.y var compact_desktop := layout_width < 900.0 var mobile_web := false if OS.has_feature("web"): @@ -505,7 +508,16 @@ func _update_responsive_layout() -> void: theme_button.custom_minimum_size = Vector2(44, 44) theme_button.position = Vector2(-58, 14) theme_button.add_theme_font_size_override("font_size", 20) - auth_panel.custom_minimum_size = Vector2(640, 790) if compact_desktop else Vector2(710, 820) + # 1000/1010 comfortably fits every view (login/register/reset/steam) + # without the inner ScrollContainer kicking in, on any window tall + # enough to hold it - but clamp to whatever height is actually + # available so a short window shrinks the panel (and lets the + # ScrollContainer take over) instead of the panel overflowing off + # the top/bottom of the screen. + var available_panel_height := layout_height - 56.0 + var target_height := 1010.0 if compact_desktop else 1000.0 + var panel_height := clampf(available_panel_height, 400.0, target_height) + auth_panel.custom_minimum_size = Vector2(640 if compact_desktop else 710, panel_height) panel_gutter.add_theme_constant_override("margin_right", 28) _apply_compact_scale(panel_content, 1.0) _apply_theme() From 854c887a1a3dcf11ced1a8039e0d0f3709321f48 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 09:10:19 -0500 Subject: [PATCH 03/12] Center login panel content, pin legal links to the bottom Making the auth panel a fixed height (to kill the scrollbar) left the shorter views (login, reset) stranded at the top with dead space below. Split panel_content into a main_column (header, message, mode views - expand-fill, center-aligned) and the legal footer as a sibling that stays pinned to the panel's bottom edge regardless of which view is showing or how tall its content is. Verified headlessly: top/bottom whitespace above and below the active view's content is now equal (e.g. login 174/174, steam 2/2), and the footer's position doesn't move between modes. --- scripts/ui/login_screen.gd | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 7f0c399..3ccb6af 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -196,16 +196,27 @@ func _build_ui() -> void: auth_panel.add_child(scroll) panel_gutter = MarginContainer.new() panel_gutter.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel_gutter.size_flags_vertical = Control.SIZE_EXPAND_FILL panel_gutter.add_theme_constant_override("margin_right", 28) scroll.add_child(panel_gutter) panel_content = VBoxContainer.new() panel_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL - panel_content.add_theme_constant_override("separation", 16) + panel_content.size_flags_vertical = Control.SIZE_EXPAND_FILL + panel_content.add_theme_constant_override("separation", 24) panel_gutter.add_child(panel_content) + # main_column holds everything except the legal footer and is the part + # that should center vertically when a view (e.g. login) is shorter than + # the panel - the footer stays pinned to the very bottom either way. + var main_column := VBoxContainer.new() + main_column.size_flags_vertical = Control.SIZE_EXPAND_FILL + main_column.alignment = BoxContainer.ALIGNMENT_CENTER + main_column.add_theme_constant_override("separation", 16) + panel_content.add_child(main_column) + var panel_header := HBoxContainer.new() panel_header.add_theme_constant_override("separation", 18) - panel_content.add_child(panel_header) + main_column.add_child(panel_header) var small_icon := TextureRect.new() small_icon.texture = Brand.ranaze_mark_texture() small_icon.custom_minimum_size = Vector2(72, 72) @@ -229,14 +240,14 @@ func _build_ui() -> void: reset_view = _build_reset_view() steam_view = _build_steam_view() for view in [login_view, register_view, reset_view, steam_view]: - panel_content.add_child(view) + main_column.add_child(view) message_label = Label.new() message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER message_label.add_theme_font_size_override("font_size", 14) message_label.add_theme_font_override("font", Brand.font) - panel_content.add_child(message_label) + main_column.add_child(message_label) var legal_footer := HBoxContainer.new() legal_footer.alignment = BoxContainer.ALIGNMENT_CENTER legal_footer.add_theme_constant_override("separation", 12) From bb9f59b3ccf9fd1309ca053b1fcd6e3e373c84ff Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 09:14:27 -0500 Subject: [PATCH 04/12] Center STEP 3 TRAINER under the RanaZe wordmark .loader-wordmark had no explicit width or text-align, so as a flex item (align-items: center) it shrink-wrapped to whatever width the "RanaZe" inline spans needed. wordmark-trainer's display:block then filled that same ambiguous width rather than the wordmark block's true rendered box, leaving it off-center relative to "RanaZe" above it. Giving .loader-wordmark an explicit width:100% and text-align: center makes both lines center within the same known-width box. Verified in a real browser: both elements now report identical getBoundingClientRect() centers (x=960 in a 1920px viewport). --- web/medmind_shell.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/medmind_shell.html b/web/medmind_shell.html index 257f86c..c6c5b4b 100644 --- a/web/medmind_shell.html +++ b/web/medmind_shell.html @@ -180,6 +180,8 @@ letter-spacing: 0; line-height: 1; margin: 12px 0 0; + text-align: center; + width: 100%; } .wordmark-rana { From 7be83661f9a5a479d89693d4c68fe801967c7c12 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 09:34:44 -0500 Subject: [PATCH 05/12] Standardize password reveal to the in-field eye icon everywhere Register, reset, and Steam-linking forms used a separate "Show password" CheckButton row; the main login form already had the eye icon embedded in the field itself. Generalized _add_password_reveal (previously hardwired to a single tracked button) to support multiple fields via password_reveal_entries, and used it for every password field instead. Removed the now-unused _add_show_password. Verified all 6 password fields (login, register x2, reset x2, steam) get a working reveal button and there's no leftover "Show password" checkbox anywhere. --- scripts/ui/login_screen.gd | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 3ccb6af..80aee17 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -25,7 +25,7 @@ var reset_view: VBoxContainer var steam_view: VBoxContainer var login_input: LineEdit var password_input: LineEdit -var password_reveal_button: Button +var password_reveal_entries: Array[Dictionary] = [] var registration_username_input: LineEdit var registration_email_input: LineEdit var registration_password_input: LineEdit @@ -295,7 +295,8 @@ func _build_register_view() -> VBoxContainer: registration_email_input = _add_field(view, "Email", "name@example.com") registration_password_input = _add_field(view, "Password", "At least 8 characters", true) registration_confirm_input = _add_field(view, "Confirm password", "Re-enter your password", true) - _add_show_password(view, [registration_password_input, registration_confirm_input]) + _add_password_reveal(registration_password_input) + _add_password_reveal(registration_confirm_input) registration_legal_check = _add_legal_consent(view) _add_primary_button(view, "Create Account", _on_register_pressed) _add_back_button(view, "Already have an account? Sign in", "login") @@ -313,7 +314,8 @@ func _build_reset_view() -> VBoxContainer: reset_code_input.max_length = 6 reset_password_input = _add_field(reset_confirm_group, "New password", "At least 8 characters", true) reset_confirm_input = _add_field(reset_confirm_group, "Confirm new password", "Re-enter your new password", true) - _add_show_password(reset_confirm_group, [reset_password_input, reset_confirm_input]) + _add_password_reveal(reset_password_input) + _add_password_reveal(reset_confirm_input) _add_primary_button(reset_confirm_group, "Update Password", _confirm_reset) _add_back_button(view, "Back to sign in", "login") reset_confirm_group.visible = false @@ -334,7 +336,7 @@ func _build_steam_view() -> VBoxContainer: steam_username_input = _add_field(view, "Username or email", "Choose a username") steam_email_input = _add_field(view, "Email", "name@example.com") steam_password_input = _add_field(view, "Password", "At least 8 characters", true) - _add_show_password(view, [steam_password_input]) + _add_password_reveal(steam_password_input) steam_legal_check = _add_legal_consent(view) _add_primary_button(view, "Continue", _complete_steam) _add_back_button(view, "Cancel external sign in", "login") @@ -407,19 +409,9 @@ func _add_field(parent: VBoxContainer, label_text: String, placeholder: String, return input -func _add_show_password(parent: VBoxContainer, targets: Array) -> void: - var toggle := CheckButton.new() - toggle.text = "Show password" - toggle.toggled.connect(func(should_reveal: bool): - for target in targets: - target.secret = not should_reveal - ) - parent.add_child(toggle) - - func _add_password_reveal(input: LineEdit) -> void: var reveal := Button.new() - password_reveal_button = reveal + password_reveal_entries.append({"input": input, "button": reveal}) reveal.text = "" reveal.tooltip_text = "Show password" reveal.flat = true @@ -588,8 +580,8 @@ func _apply_theme() -> void: theme_button.add_theme_stylebox_override("normal", theme_normal) theme_button.add_theme_stylebox_override("hover", theme_normal) theme_button.add_theme_color_override("font_color", Brand.AMBER) - if password_reveal_button != null: - _refresh_password_reveal(password_input, password_reveal_button) + for entry in password_reveal_entries: + _refresh_password_reveal(entry["input"], entry["button"]) subheading.add_theme_color_override("font_color", Brand.muted_color(dark)) message_label.add_theme_color_override("font_color", Brand.muted_color(dark)) if brand_tagline != null: From ba8e0ec2564bd675ef84c04ffa3f59099ffb971a Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 09:36:06 -0500 Subject: [PATCH 06/12] Remove duplicate Terms/Privacy links from account creation Register and Steam-linking forms rendered their own Terms of Service/Privacy Policy links right under the consent checkbox, on top of the ones already pinned to the bottom of the panel. The consent checkbox text alone is enough; the persistent footer covers the links. --- scripts/ui/login_screen.gd | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 80aee17..2617b4e 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -348,10 +348,6 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: check.text = "I agree to the Terms of Service and acknowledge the Privacy Policy." check.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART parent.add_child(check) - var links := HBoxContainer.new() - links.add_theme_constant_override("separation", 12) - parent.add_child(links) - _add_legal_links(links) return check From 7e977e0d30cf2e8273e1094864e2de6e9c10f633 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 09:52:41 -0500 Subject: [PATCH 07/12] Clean up the legal consent checkbox layout A bare CheckBox's word-wrap left-aligns every wrapped line flush with the checkbox glyph, so "Privacy Policy." on its own line read as unindented and sloppy next to an otherwise fully themed form. Split the toggle from its label (a separate Label wraps within its own column instead, so wrapped lines stay aligned under the first line) and wrapped the row in a bordered card matching the app's field styling. Clicking the label also toggles the checkbox now, for a bigger hit target. Verified with an actual rendered screenshot: wrapped text now indents consistently, and both checked/unchecked states are legible. --- scripts/ui/login_screen.gd | 44 +++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 2617b4e..ccd4ad3 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -344,10 +344,36 @@ func _build_steam_view() -> VBoxContainer: func _add_legal_consent(parent: VBoxContainer) -> CheckBox: + # A bare CheckBox's own word-wrap left-aligns every wrapped line flush + # with the checkbox glyph, so a second line reads as un-indented and + # sloppy. Split the toggle from its label into a row instead - the + # label's own wrapping then stays confined to its column, and the whole + # thing sits in a card so it doesn't look like a stray unstyled control + # next to the app's otherwise fully themed form fields. + var card := PanelContainer.new() + card.set_meta("legal_consent_card", true) + parent.add_child(card) + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_left", 16) + margin.add_theme_constant_override("margin_right", 16) + margin.add_theme_constant_override("margin_top", 12) + margin.add_theme_constant_override("margin_bottom", 12) + card.add_child(margin) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 12) + margin.add_child(row) var check := CheckBox.new() - check.text = "I agree to the Terms of Service and acknowledge the Privacy Policy." - check.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - parent.add_child(check) + row.add_child(check) + var label := Label.new() + label.text = "I agree to the Terms of Service and acknowledge the Privacy Policy." + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.mouse_filter = Control.MOUSE_FILTER_STOP + label.gui_input.connect(func(event: InputEvent): + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + check.button_pressed = not check.button_pressed + ) + row.add_child(label) return check @@ -555,6 +581,9 @@ func _apply_theme() -> void: label.add_theme_color_override("font_color", Brand.text_color(dark)) for input in fields: _style_auth_field(input, dark) + for node in find_children("*", "PanelContainer", true, false): + if node.has_meta("legal_consent_card"): + (node as PanelContainer).add_theme_stylebox_override("panel", _legal_consent_box(dark)) for button in find_children("*", "Button", true, false): var typed := button as Button typed.add_theme_font_override("font", Brand.font) @@ -584,6 +613,15 @@ func _apply_theme() -> void: brand_tagline.add_theme_color_override("default_color", Brand.muted_color(dark)) +func _legal_consent_box(dark: bool) -> StyleBoxFlat: + var box := StyleBoxFlat.new() + box.bg_color = Color("0d172a") if dark else Brand.LIGHT_SURFACE + box.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER + box.set_border_width_all(1) + box.set_corner_radius_all(10) + return box + + func _style_auth_field(input: LineEdit, dark: bool) -> void: input.add_theme_color_override("font_color", Brand.text_color(dark)) input.add_theme_color_override("font_uneditable_color", Brand.text_color(dark)) From 5a2d825181970df9fffcccbd39a5450e1ca879a8 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 10:13:18 -0500 Subject: [PATCH 08/12] Rework legal consent to match the provided design Drop the bordered card - it wasn't the right direction. Instead: - Plain small checkbox with just a blue border (no fill/icon when unchecked), matching the reference exactly. - "Terms of Service"/"Privacy Policy" render as inline blue links within the sentence (RichTextLabel + BBCode [url], routed through the same _open_legal_document as the footer), instead of a duplicate link row underneath. meta_underlined = false to match the reference's plain (non-underlined) link style. Verified with real rendered screenshots at each step, including a pixel-level crop confirming the underline was actually gone. --- scripts/ui/login_screen.gd | 80 ++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index ccd4ad3..036ef07 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -26,6 +26,7 @@ var steam_view: VBoxContainer var login_input: LineEdit var password_input: LineEdit var password_reveal_entries: Array[Dictionary] = [] +var legal_consent_texts: Array[RichTextLabel] = [] var registration_username_input: LineEdit var registration_email_input: LineEdit var registration_password_input: LineEdit @@ -345,38 +346,45 @@ func _build_steam_view() -> VBoxContainer: func _add_legal_consent(parent: VBoxContainer) -> CheckBox: # A bare CheckBox's own word-wrap left-aligns every wrapped line flush - # with the checkbox glyph, so a second line reads as un-indented and - # sloppy. Split the toggle from its label into a row instead - the - # label's own wrapping then stays confined to its column, and the whole - # thing sits in a card so it doesn't look like a stray unstyled control - # next to the app's otherwise fully themed form fields. - var card := PanelContainer.new() - card.set_meta("legal_consent_card", true) - parent.add_child(card) - var margin := MarginContainer.new() - margin.add_theme_constant_override("margin_left", 16) - margin.add_theme_constant_override("margin_right", 16) - margin.add_theme_constant_override("margin_top", 12) - margin.add_theme_constant_override("margin_bottom", 12) - card.add_child(margin) + # with the checkbox glyph, so a second line reads as un-indented. Split + # the toggle from its text into a row instead - the RichTextLabel's own + # wrapping then stays confined to its column, and "Terms of Service"/ + # "Privacy Policy" render as inline links rather than a duplicate row + # of link buttons underneath. var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) - margin.add_child(row) + parent.add_child(row) var check := CheckBox.new() + check.custom_minimum_size = Vector2(26, 26) + check.add_theme_icon_override("unchecked", _blank_icon()) row.add_child(check) - var label := Label.new() - label.text = "I agree to the Terms of Service and acknowledge the Privacy Policy." - label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - label.mouse_filter = Control.MOUSE_FILTER_STOP - label.gui_input.connect(func(event: InputEvent): - if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - check.button_pressed = not check.button_pressed - ) - row.add_child(label) + var consent_text := RichTextLabel.new() + consent_text.bbcode_enabled = true + consent_text.fit_content = true + consent_text.scroll_active = false + consent_text.meta_underlined = false + consent_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL + consent_text.add_theme_font_override("normal_font", Brand.font) + consent_text.add_theme_font_size_override("normal_font_size", 15) + consent_text.meta_clicked.connect(func(meta: Variant): _open_legal_document(str(meta))) + legal_consent_texts.append(consent_text) + row.add_child(consent_text) return check +func _blank_icon() -> ImageTexture: + return ImageTexture.create_from_image(Image.create(1, 1, false, Image.FORMAT_RGBA8)) + + +func _refresh_legal_consent_text(consent_text: RichTextLabel, dark: bool) -> void: + var text_hex := Brand.text_color(dark).to_html(false) + var link_hex := (Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE).to_html(false) + consent_text.text = ( + "[color=#%s]I agree to the [/color][url=terms][color=#%s]Terms of Service[/color][/url][color=#%s] and acknowledge the [/color][url=privacy][color=#%s]Privacy Policy[/color][/url][color=#%s].[/color]" + % [text_hex, link_hex, text_hex, link_hex, text_hex] + ) + + func _add_legal_links(parent: HBoxContainer) -> void: for spec in [["Terms of Service", "terms"], ["Privacy Policy", "privacy"]]: var link := Button.new() @@ -581,9 +589,16 @@ func _apply_theme() -> void: label.add_theme_color_override("font_color", Brand.text_color(dark)) for input in fields: _style_auth_field(input, dark) - for node in find_children("*", "PanelContainer", true, false): - if node.has_meta("legal_consent_card"): - (node as PanelContainer).add_theme_stylebox_override("panel", _legal_consent_box(dark)) + for consent_text in legal_consent_texts: + _refresh_legal_consent_text(consent_text, dark) + var checkbox_box := StyleBoxFlat.new() + checkbox_box.bg_color = Color.TRANSPARENT + checkbox_box.border_color = Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE + checkbox_box.set_border_width_all(2) + checkbox_box.set_corner_radius_all(6) + for cb in find_children("*", "CheckBox", true, false): + for state in ["normal", "hover", "pressed", "focus"]: + cb.add_theme_stylebox_override(state, checkbox_box) for button in find_children("*", "Button", true, false): var typed := button as Button typed.add_theme_font_override("font", Brand.font) @@ -613,15 +628,6 @@ func _apply_theme() -> void: brand_tagline.add_theme_color_override("default_color", Brand.muted_color(dark)) -func _legal_consent_box(dark: bool) -> StyleBoxFlat: - var box := StyleBoxFlat.new() - box.bg_color = Color("0d172a") if dark else Brand.LIGHT_SURFACE - box.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER - box.set_border_width_all(1) - box.set_corner_radius_all(10) - return box - - func _style_auth_field(input: LineEdit, dark: bool) -> void: input.add_theme_color_override("font_color", Brand.text_color(dark)) input.add_theme_color_override("font_uneditable_color", Brand.text_color(dark)) From bff16cea9829761c0265405651d1a4441283ffe9 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 10:26:35 -0500 Subject: [PATCH 09/12] Fix oversized, borderless consent checkbox The generic "style every Button" pass in _apply_theme also matched CheckBox (it's a Button subtype) and ran after the checkbox-specific styling, overwriting the intended blue border with the plain default button box - and that box's own content margins were inflating the checkbox well past its 26x26 minimum (actual rendered size was 60x52). Skip CheckBox in that generic loop, zero out the checkbox stylebox's content margins, shrink it to 22x22, and stop it stretching to the row's full height. Also sized the consent text down (15->13) and gave the row some vertical breathing room. Verified with a rendered screenshot and a direct stylebox/size dump: checkbox is now 22x24 with a real 2px blue border, matching the provided reference. --- scripts/ui/login_screen.gd | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 036ef07..eefbc2b 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -351,11 +351,17 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: # wrapping then stays confined to its column, and "Terms of Service"/ # "Privacy Policy" render as inline links rather than a duplicate row # of link buttons underneath. + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_top", 8) + margin.add_theme_constant_override("margin_bottom", 8) + parent.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) - parent.add_child(row) + margin.add_child(row) var check := CheckBox.new() - check.custom_minimum_size = Vector2(26, 26) + check.name = "LegalConsentCheckBox" + check.custom_minimum_size = Vector2(22, 22) + check.size_flags_vertical = Control.SIZE_SHRINK_BEGIN check.add_theme_icon_override("unchecked", _blank_icon()) row.add_child(check) var consent_text := RichTextLabel.new() @@ -365,7 +371,7 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: consent_text.meta_underlined = false consent_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL consent_text.add_theme_font_override("normal_font", Brand.font) - consent_text.add_theme_font_size_override("normal_font_size", 15) + consent_text.add_theme_font_size_override("normal_font_size", 13) consent_text.meta_clicked.connect(func(meta: Variant): _open_legal_document(str(meta))) legal_consent_texts.append(consent_text) row.add_child(consent_text) @@ -596,10 +602,13 @@ func _apply_theme() -> void: checkbox_box.border_color = Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE checkbox_box.set_border_width_all(2) checkbox_box.set_corner_radius_all(6) + checkbox_box.set_content_margin_all(0) for cb in find_children("*", "CheckBox", true, false): for state in ["normal", "hover", "pressed", "focus"]: cb.add_theme_stylebox_override(state, checkbox_box) for button in find_children("*", "Button", true, false): + if button is CheckBox: + continue var typed := button as Button typed.add_theme_font_override("font", Brand.font) typed.add_theme_color_override("font_color", Brand.text_color(dark)) From eb9e018339ec686ec5ee76eed293bc66e1dfe850 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 10:45:45 -0500 Subject: [PATCH 10/12] Fix non-square consent checkbox with a dedicated Panel square Root cause: CheckBox/Button reserves vertical space for a text baseline even with an empty string, so no amount of custom_minimum_size or stylebox content-margin tweaking produced a true square - it just came out close-but-not-quite in some layouts and visibly elongated in others. Decoupled the visual square from the CheckBox entirely: a plain Panel (exactly 20x20, toggled by direct click) now draws the border/fill, while a real (invisible) CheckBox still holds .button_pressed for the rest of the code to read. Also added left padding on the row and vertically centered both the square and the consent text within it. Verified with a direct size dump (20x20 in both checked/unchecked states, where it previously measured non-square) and rendered screenshots of both states. --- scripts/ui/login_screen.gd | 63 ++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index eefbc2b..0ca8ca5 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -27,6 +27,7 @@ var login_input: LineEdit var password_input: LineEdit var password_reveal_entries: Array[Dictionary] = [] var legal_consent_texts: Array[RichTextLabel] = [] +var legal_consent_squares: Array[Panel] = [] var registration_username_input: LineEdit var registration_email_input: LineEdit var registration_password_input: LineEdit @@ -351,25 +352,47 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: # wrapping then stays confined to its column, and "Terms of Service"/ # "Privacy Policy" render as inline links rather than a duplicate row # of link buttons underneath. + # + # The actual toggle state lives on a real CheckBox (kept invisible and + # out of the row's layout) because other code reads .button_pressed - + # but Button/CheckBox reserves vertical space for a text baseline even + # with an empty string, which was making the visible square non-square + # no matter what custom_minimum_size said. Drawing the visible square + # as a plain Panel sidesteps that entirely. + var check := CheckBox.new() + check.visible = false + parent.add_child(check) + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_left", 6) margin.add_theme_constant_override("margin_top", 8) margin.add_theme_constant_override("margin_bottom", 8) parent.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) margin.add_child(row) - var check := CheckBox.new() - check.name = "LegalConsentCheckBox" - check.custom_minimum_size = Vector2(22, 22) - check.size_flags_vertical = Control.SIZE_SHRINK_BEGIN - check.add_theme_icon_override("unchecked", _blank_icon()) - row.add_child(check) + + var square := Panel.new() + square.custom_minimum_size = Vector2(20, 20) + square.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + square.size_flags_vertical = Control.SIZE_SHRINK_CENTER + square.mouse_filter = Control.MOUSE_FILTER_STOP + square.set_meta("legal_consent_checkbox", check) + square.gui_input.connect(func(event: InputEvent): + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + check.button_pressed = not check.button_pressed + ) + row.add_child(square) + legal_consent_squares.append(square) + check.toggled.connect(func(_pressed: bool): _refresh_legal_consent_square(square)) + var consent_text := RichTextLabel.new() consent_text.bbcode_enabled = true consent_text.fit_content = true consent_text.scroll_active = false consent_text.meta_underlined = false consent_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL + consent_text.size_flags_vertical = Control.SIZE_SHRINK_CENTER consent_text.add_theme_font_override("normal_font", Brand.font) consent_text.add_theme_font_size_override("normal_font_size", 13) consent_text.meta_clicked.connect(func(meta: Variant): _open_legal_document(str(meta))) @@ -378,10 +401,6 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: return check -func _blank_icon() -> ImageTexture: - return ImageTexture.create_from_image(Image.create(1, 1, false, Image.FORMAT_RGBA8)) - - func _refresh_legal_consent_text(consent_text: RichTextLabel, dark: bool) -> void: var text_hex := Brand.text_color(dark).to_html(false) var link_hex := (Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE).to_html(false) @@ -391,6 +410,19 @@ func _refresh_legal_consent_text(consent_text: RichTextLabel, dark: bool) -> voi ) +func _refresh_legal_consent_square(square: Panel) -> void: + var dark := UIThemeService.is_dark_mode() + var accent := Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE + var checked := square.get_meta("legal_consent_checkbox", null) as CheckBox + var is_checked := checked != null and checked.button_pressed + var box := StyleBoxFlat.new() + box.bg_color = accent if is_checked else Color.TRANSPARENT + box.border_color = accent + box.set_border_width_all(2) + box.set_corner_radius_all(5) + square.add_theme_stylebox_override("panel", box) + + func _add_legal_links(parent: HBoxContainer) -> void: for spec in [["Terms of Service", "terms"], ["Privacy Policy", "privacy"]]: var link := Button.new() @@ -597,15 +629,8 @@ func _apply_theme() -> void: _style_auth_field(input, dark) for consent_text in legal_consent_texts: _refresh_legal_consent_text(consent_text, dark) - var checkbox_box := StyleBoxFlat.new() - checkbox_box.bg_color = Color.TRANSPARENT - checkbox_box.border_color = Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE - checkbox_box.set_border_width_all(2) - checkbox_box.set_corner_radius_all(6) - checkbox_box.set_content_margin_all(0) - for cb in find_children("*", "CheckBox", true, false): - for state in ["normal", "hover", "pressed", "focus"]: - cb.add_theme_stylebox_override(state, checkbox_box) + for square in legal_consent_squares: + _refresh_legal_consent_square(square) for button in find_children("*", "Button", true, false): if button is CheckBox: continue From 6b1e41c187ab667840831bf2dbb77174842ab86e Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 10:53:53 -0500 Subject: [PATCH 11/12] Fix loading-screen sizing shift from the Inter font swap font-display: swap paints with the fallback font stack first, then swaps to Inter once the 876KB variable font finishes downloading. .loader-wordmark was already width-fixed from an earlier centering fix, but .loader-tagline, .loader-message, and .progress-percent had no explicit width and (more importantly) used line-height: normal, which is derived from each font's own metrics - Inter and the fallback stack disagree by 3-4px per line. Those three deltas summed exactly to the .loader column's total 10px height change, which is what visibly shifted the whole centered loading screen once the swap happened. Gave the width-dependent elements width: 100% (matching the wordmark) and gave all three text elements an explicit unitless line-height (computed from font-size, not font metrics) so their box size no longer depends on which font is currently active. Verified empirically: forced the fallback font stack on the live page and diffed getBoundingClientRect() against Inter for every loader element - all width/height deltas are 0 after the fix (previously up to 17.5px width / 10px cascaded height). --- web/medmind_shell.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/medmind_shell.html b/web/medmind_shell.html index c6c5b4b..345dbfe 100644 --- a/web/medmind_shell.html +++ b/web/medmind_shell.html @@ -209,7 +209,9 @@ font-size: clamp(20px, 2.3vw, 28px); font-weight: 430; letter-spacing: 0; + line-height: 1.3; margin: 17px 0 clamp(36px, 6vh, 54px); + width: 100%; } .progress-row { @@ -241,6 +243,7 @@ font-size: 22px; font-variant-numeric: tabular-nums; font-weight: 650; + line-height: 1.3; min-width: 60px; } @@ -248,8 +251,10 @@ color: var(--muted); font-size: 19px; letter-spacing: 0; + line-height: 1.3; margin: 24px 0 0; text-align: center; + width: 100%; } #status-notice { From aa5497c5169fbb5ee415dd02203096e5b9637427 Mon Sep 17 00:00:00 2001 From: hz Date: Sat, 25 Jul 2026 11:00:48 -0500 Subject: [PATCH 12/12] Center the legal consent row instead of left-aligning it Wrapped the row in a CenterContainer so the checkbox+text sit centered under the field column instead of hugging the left edge. Had to drop the RichTextLabel's EXPAND_FILL (meaningless inside a CenterContainer, which never stretches its child) in favor of an explicit 480px minimum width - without one, the label collapsed toward zero width and wrapped character-by-character, which is what the first attempt at this actually rendered as before I caught it. Verified with a direct coordinate check: the row's horizontal center now matches the confirm-password field's center exactly (delta 0.0). --- scripts/ui/login_screen.gd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 0ca8ca5..8670cb8 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -364,13 +364,14 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: parent.add_child(check) var margin := MarginContainer.new() - margin.add_theme_constant_override("margin_left", 6) margin.add_theme_constant_override("margin_top", 8) margin.add_theme_constant_override("margin_bottom", 8) parent.add_child(margin) + var center := CenterContainer.new() + margin.add_child(center) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) - margin.add_child(row) + center.add_child(row) var square := Panel.new() square.custom_minimum_size = Vector2(20, 20) @@ -391,7 +392,7 @@ func _add_legal_consent(parent: VBoxContainer) -> CheckBox: consent_text.fit_content = true consent_text.scroll_active = false consent_text.meta_underlined = false - consent_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL + consent_text.custom_minimum_size = Vector2(480, 0) consent_text.size_flags_vertical = Control.SIZE_SHRINK_CENTER consent_text.add_theme_font_override("normal_font", Brand.font) consent_text.add_theme_font_size_override("normal_font_size", 13)