Skip to Content
Pending: 0 Building: 0 Running: 0 Failed: 19
Created Date Type Name Commit Description State Age Up Time Life Time Action
merged [FIX] viin_backend_theme: keep apps menu open while app reloads [FIX] viin_backend_theme: keep apps menu open while app reloads Opening the apps menu from the navbar while an app was still reloading (for example clicking the app brand and then the apps button before the screen finished loading) closed the menu before its tiles appeared, so the user saw nothing to pick an app from. It broke the runbot hr_expense tour create_expense_no_employee_access_tour (step 13/13, .o_app tile not found, 4 failures out of 5 runs). Cause: NavBar.setup() closed the menu overlay on every ACTION_MANAGER:UI-UPDATED. The reload started by the brand click finished while the overlay's ViinHomeMenu was still in onWillStart (its mail.activity search_count and res.users read RPCs), so the overlay entry was removed before any tile was mounted. The UI-UPDATED listener is removed. The menu is still closed by a route change, outside click, Escape and when the navbar is destroyed; the mobile "All Apps" guard that refuses to open a menu before any screen exists is unchanged. Tests: a Hoot desktop test holds the list reload and the menu's own RPCs and requires the overlay and its app tiles to be on screen once both finish; it fails with the listener and passes without it. The mobile test that faked a screen to assert the removed close is dropped; the real guard stays covered by "All Apps tapped while the boot app menu is still loading opens no overlay and the toggle keeps working". Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
merged [FWD] 18.0 -> 19.0: de-brand POS/Inventory/Manufacturing, mobile apps menu [FIX] viin_brand_mrp: neutral operation cost help The "Cost based on" help of BoM operations no longer names Odoo, in English or Vietnamese. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
merged [FWD] 18.0 -> 19.0: de-brand POS/Inventory/Manufacturing, mobile apps menu [FIX] viin_brand_mrp: neutral operation cost help The "Cost based on" help of BoM operations no longer names Odoo, in English or Vietnamese. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
merged [I18N] *: align Vietnamese terminology with the corrected core catalogs [I18N] *: align Vietnamese terminology with the corrected core catalogs Odoo 19 core's Vietnamese catalogs were corrected against the glossary researched per business domain (Viindoo/odoo#1302). This brings this repository's catalogs in line, so a Vietnamese user sees one term for one concept across core and Viindoo modules. The 18.0 alignment came over with the forward-port; this covers the strings new in 19.0 and the terms decided for 19.0: - Records shared with core (model names, re-declared fields, selection values) now carry core's 19.0 wording exactly; before, the module loaded last decided which label users saw. - 19.0 terms: weekdays in word form (Thứ Hai ... Chủ Nhật). - The approved glossary applied in context to strings new in 19.0 (Phiếu kho, Loại nghiệp vụ, Cổng thanh toán, Đóng kiện, ...), and debranded help texts say "hệ thống" where the source does, not "Odoo". - Definite errors fixed: reversed or wrong meanings, broken placeholders and markup, English left in translations, garbled text and typos. - Old-style tone marks throughout (hóa, hủy, khóa), ASCII hyphens. Only msgstr content changes; untranslated entries are left as they are. Failed
merged Fwd from 18 to 19 261009 01 Merge remote-tracking branch 'origin/18.0' into 19.0 Range 4a5885d (1 commit). Conflict resolution: - i18n/vi_VN.po: kept the 19.0 catalog shape and entries; where 18.0 changed a msgstr and the same msgid exists on 19.0, the 18.0 wording wins. - viin_brand_common (renamed viin_brand_base_setup on 19.0): its 18.0 msgstr change applied to viin_brand_base_setup/i18n/vi_VN.po. - viin_brand_membership (removed on 19.0): catalog stays deleted. Failed
merged [FIX] viin_brand_web: scope the navbar hover/active hook guard to the regular navbar [FIX] viin_brand_web: scope the navbar hover/active hook guard to the regular navbar test_navbar_menu_entry_interactive_states_use_deep_chrome_rung asserted that core never DECLARES --NavBar-entry-backgroundColor--hover/--active, because the var() fallback those hooks read is what carries the brand's deep chrome rung. It checked that with a substring scan over the whole compiled web.assets_backend, so any declaration anywhere tripped it. viin_customizer declares both hooks to theme its own dark top bar: `.customizer_container .customizer_navbar .o_main_navbar { ... }`. That rule never applies to the regular web client navbar, so the brand rung there is untouched, yet the scan reported it as core re-declaring the hook and the test went red as soon as viin_customizer was installed. The guard now collects only the declarations whose selector can reach the regular navbar entries - the entry itself or any ancestor on the NAVBAR_ANCESTORS chain, including class-less subjects such as :root. Custom properties inherit, so an ancestor-level declaration counts; a declaration under a context the navbar is not in does not. The check cannot be left to the cascade resolution further down: that walks only the entry, so a `.o_main_navbar` declaration inherited from above would stay invisible to it. Verified on 19.0 with viin_brand_web and viin_customizer installed: the old guard fails with "'--NavBar-entry-backgroundColor--hover:' unexpectedly found", the new one passes, and temporarily adding `.o_main_navbar { --NavBar-entry-backgroundColor--hover: #00bbce }` to the brand bundle still makes it fail. Also reword the docstring of viin_backend_theme's test_navbar_apps_icon_computes_cores_navbar_entry_colour. It said nothing in the addons path declares --NavBar-entry-color, which stops being true once viin_customizer is installed. It now says no rule that reaches the regular navbar declares it, and that viin_customizer's declaration lives only under its own .customizer_navbar scope, which the cascade resolver excludes. Docstring only; that test class still passes with viin_customizer installed. Signed-off-by: David Tran <david.tran@tvtmarine.com> Killed
merged [FIX] viin_brand_web: scope the navbar hover/active hook guard to the regular navbar [FIX] viin_brand_web: scope the navbar hover/active hook guard to the regular navbar test_navbar_menu_entry_interactive_states_use_deep_chrome_rung asserted that core never DECLARES --NavBar-entry-backgroundColor--hover/--active, because the var() fallback those hooks read is what carries the brand's deep chrome rung. It checked that with a substring scan over the whole compiled web.assets_backend, so any declaration anywhere tripped it. viin_customizer declares both hooks to theme its own dark top bar: `.customizer_container .customizer_navbar .o_main_navbar { ... }`. That rule never applies to the regular web client navbar, so the brand rung there is untouched, yet the scan reported it as core re-declaring the hook and the test went red as soon as viin_customizer was installed. The guard now collects only the declarations whose selector can reach the regular navbar entries - the entry itself or any ancestor on the NAVBAR_ANCESTORS chain, including class-less subjects such as :root. Custom properties inherit, so an ancestor-level declaration counts; a declaration under a context the navbar is not in does not. The check cannot be left to the cascade resolution further down: that walks only the entry, so a `.o_main_navbar` declaration inherited from above would stay invisible to it. Verified on 19.0 with viin_brand_web and viin_customizer installed: the old guard fails with "'--NavBar-entry-backgroundColor--hover:' unexpectedly found", the new one passes, and temporarily adding `.o_main_navbar { --NavBar-entry-backgroundColor--hover: #00bbce }` to the brand bundle still makes it fail. Signed-off-by: David Tran <david.tran@tvtmarine.com> Killed
merged [FIX] viin_brand_pos: Fix branding issues in customer facing display [FIX] viin_brand_pos: Fix branding issues in customer facing display Killed
merged [FIX] viin_brand_website, viin_backend_theme: one click into the backend, and type to search [IMP] viin_brand_website: one click from the site into the backend Clicking the apps grid on the website used to open a dropdown listing the internal user's apps, so reaching the backend cost an extra click every time. The button now links straight to /odoo and no longer toggles anything. /odoo can take a noticeable moment on an install with many modules, so the icon swaps to a spinner on click: the user can see the click registered. A Ctrl/Cmd/Shift or middle click opens the backend in a new tab and leaves the current page's icon alone, and a Back-button restore from the browser's cache puts the icon back rather than leaving it spinning for good. That spinner is wired by a delegated listener on the document, not by a public Interaction. It was written as an Interaction first and it never ran: the interaction service scans from #wrapwrap, and this button renders outside it, under body > .o_frontend_to_backend_nav > .o_frontend_to_backend_buttons. Core drives this same bar from a plain frontend module for the same reason. Both click and auxclick are bound, because a non-primary click fires the latter and core treats the two as a pair. The spinner is a dedicated child element rather than classes on the anchor, and that distinction is the whole of the difference. Core renders that anchor as both the icon and a styled flex box, so a spin class on it rotates the box - background, padding and all - which is what it did at first. Every pending indicator in Odoo 19 puts fa fa-circle-o-notch fa-spin on an element whose only job is to be the spinner, and this now does the same. Measured with the animation frozen at one angle: the old anchor's bounding box grew from 48x54 to 72x72 as the box turned; the anchor now holds 48x54, identical to an untouched button, and only the glyph inside it moves. The tour asserts the anchor carries no spin class, so putting one back there fails loudly instead of passing. Core's own dropdown markup beside the button is left exactly as core renders it, including the ir.ui.menu lookup it performs on every frontend page. Taking it out looked like a free win and was tried twice; core's website_backend_menus_redirect tour opens that dropdown and clicks an entry inside it, and the entry only exists when the lookup runs. The de-branded title on the button is unchanged. Tests: the click, modifier and cache-restore behaviour are covered on a real DOM rather than by matching the text of an inline handler, which an implementation with the transitions swapped passed just as happily. A tour drives the button on a real rendered page and asserts both that the icon swaps and that the button is NOT inside #wrapwrap, so if a future Odoo moves it the guard fails loudly instead of passing for the wrong reason - a unit test cannot make that assertion, because the test helper fabricates a #wrapwrap of its own, which is precisely why nine green unit tests hid a feature that did not work. Bundle guards assert the module really reaches the browser. Also here, and separable from the behaviour: comments in this module's tests cited source by line number. Each now names the symbol instead. One had already rotted badly, naming a method while pointing fifteen lines above its real definition. Killed
merged [FIX] viin_brand_website, viin_backend_theme: one click into the backend, and type to search [IMP] viin_brand_website: one click from the site into the backend Clicking the apps grid on the website used to open a dropdown listing the internal user's apps, so reaching the backend cost an extra click every time. The button now links straight to /odoo and no longer toggles anything. /odoo can take a noticeable moment on an install with many modules, so the icon swaps to a spinner on click: the user can see the click registered. A Ctrl/Cmd/Shift or middle click opens the backend in a new tab and leaves the current page's icon alone, and a Back-button restore from the browser's cache puts the icon back rather than leaving it spinning for good. That spinner is wired by a delegated listener on the document, not by a public Interaction. It was written as an Interaction first and it never ran: the interaction service scans from #wrapwrap, and this button renders outside it, under body > .o_frontend_to_backend_nav > .o_frontend_to_backend_buttons. Core drives this same bar from a plain frontend module for the same reason. Both click and auxclick are bound, because a non-primary click fires the latter and core treats the two as a pair. Core's own dropdown markup beside the button is left exactly as core renders it, including the ir.ui.menu lookup it performs on every frontend page. Taking it out looked like a free win and was tried twice; core's website_backend_menus_redirect tour opens that dropdown and clicks an entry inside it, and the entry only exists when the lookup runs. The de-branded title on the button is unchanged. Tests: the click, modifier and cache-restore behaviour are covered on a real DOM rather than by matching the text of an inline handler, which an implementation with the transitions swapped passed just as happily. A tour drives the button on a real rendered page and asserts both that the icon swaps and that the button is NOT inside #wrapwrap, so if a future Odoo moves it the guard fails loudly instead of passing for the wrong reason - a unit test cannot make that assertion, because the test helper fabricates a #wrapwrap of its own, which is precisely why nine green unit tests hid a feature that did not work. Bundle guards assert the module really reaches the browser. Also here, and separable from the behaviour: comments in this module's tests cited source by line number. Each now names the symbol instead. One had already rotted badly, naming a method while pointing fifteen lines above its real definition. Killed
merged [FIX] viin_brand_website, viin_backend_theme: one click into the backend, and type to search [FIX] viin_brand_website: wire the apps button off the DOM it is in The apps-button spinner never ran on a real page. It was written as a public Interaction, and the interaction service only ever scans from #wrapwrap, but the button sits outside it - it is a `body > .o_frontend_to_backend_nav > .o_frontend_to_backend_buttons > a`, a #wrapwrap SIBLING, never a descendant. So the interaction never started, the icon never swapped on click, and a Back-button restore never reset it. Replace it with a delegated click/auxclick listener bound on the document at load time, which does not care where the button lives. The three functions that decide what happens on click are unchanged. The nine unit tests over the old Interaction had all been passing for the wrong reason: the Hoot fixture helper wraps bare markup in a #wrapwrap of its own, so the suite was exercising the button in a position it never occupies on the real page. Those tests now mount the fixture directly against the new listener, and a tour drives the real rendered homepage - asserting both that the click swaps the icon and that the button is NOT inside #wrapwrap, so a future core move that reintroduces the mismatch fails loudly instead of passing for the wrong reason again. Failed
open [REF] viin_brand cluster: make each module installable alone [MISC] viin_brand_spreadsheet_dashboard: guard tours on web_tour Tests that boot the webclient via start_tour required the tour engine unconditionally, so they errored instead of skipping under the acceptance CLI's --skip-auto-install flag, which excludes the web_tour/bus auto-install closure. They now probe res.users for the tour_enabled field in setUp and skip cleanly with a stated reason when web_tour is not installed, so a per-module acceptance run stays readable instead of burying a genuine failure under environment-driven errors. A bundle-compile test is added, compiling this module's real asset bundles and asserting on the compiled output. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
open [REF] viin_brand cluster: make each module installable alone [MISC] pyproject: pin the repo's first ruff lint configuration Ruff was running fully unconfigured against this repository. A repo-root pyproject.toml now establishes an explicit configuration, pinning today's implicit rule set so a future ruff upgrade cannot silently change what is enforced. Signed-off-by: David Tran <david.tran@tvtmarine.com> Killed
merged [FIX] viin_brand_web: resolve dark mode from the user, not the browser [FIX] viin_brand_web: resolve dark mode from the user, not the browser Two reported symptoms, one root cause: the code treated the `color_scheme` cookie as if it were a preference, when it is a cache of a resolution. A preference belongs to a USER; this cookie belongs to a BROWSER and outlives the session that wrote it. Symptom 1 - another person's cookie decided your scheme. color_scheme() ranked the cookie above the stored preference, so on a shared browser the last user's choice served the next user the wrong compiled bundle. Measured before the change: preference 'light' plus a `color_scheme=dark` cookie rendered data-bs-theme="dark", with the dark bundle referenced. Symptom 2 - "System" always came up light. setScheme('auto') writes the matchMedia-resolved scheme to the cookie, then awaits an ORM write whose own handler expired that very cookie. Measured: the write response carried `color_scheme=; Max-Age=0; Path=/`, the host-only cookie left the jar, and the reload that follows then found no cookie, fell through to core's `return "light"`, and served light whatever the device said. The client re-resolved afterwards, so the user saw light, a half-flip, and dark only on the next reload. The resolution order is now the user's own explicit light/dark preference, then the cookie, then super(). The cookie keeps its place above super() because 'auto' is the one case the server genuinely cannot decide: it cannot read the device preference, so the client resolves it and caches the effective light|dark there. For the same reason writing 'auto' no longer expires the cookie - that would throw away the only copy of the answer the imminent reload needs. That cookie is also core's own client-side source of truth for dark mode: core reads it for graph colours, the colour picker, the ace editor and the pdf.js viewer. Expiring it did not merely mis-pick a bundle, it left every one of those drawing light-mode colours inside a dark UI. Two tests pinned the old behaviour and are replaced, not loosened. One asserted the cookie "overrides everything"; it now asserts an explicit preference survives a contrary cookie, with a sibling covering the 'auto' case where the cookie is authoritative. The other asserted the switch to 'auto' expires the cookie, citing the old precedence as its reason; it now asserts the cached resolution is left alone. Both were protecting a defect, so changing them is a corrected contract, not an expectation bent to match output. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
open [REF] viin_brand cluster: make each module installable alone [MISC] pyproject: pin the repo's first ruff lint configuration Ruff was running fully unconfigured against this repository. A repo-root pyproject.toml now establishes an explicit configuration, pinning today's implicit rule set so a future ruff upgrade cannot silently change what is enforced. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
merged [FIX] dark mode: readable pivot/graph, dark spreadsheet and dashboard chrome, calmer Discuss seams [FIX] viin_backend_theme: dedupe the active marker in the Appearance menu The Appearance systray menu's active Theme row and active Density row each drew two selected-markers: core's own `.dropdown-item.active::before` FontAwesome checkmark painted on top of this component's own right-aligned check icon, so the active choice showed a double mark instead of one. Core's marker is suppressed on both DropdownItem loops with core's own opt-out class `dropdown-item_active_noarrow`, leaving the component's own check as the single indicator. mail's action list already reuses that class for the identical situation, so this reuses an existing core lever instead of inventing new CSS. A tour-driven HttpCase asserts the rendered result - the active row's computed `::before` is 'none' and exactly one visible `.fa-check` remains inside it - in both the light and the dark colour scheme, so the contract is pinned on render output rather than on markup. Signed-off-by: David Tran <david.tran@tvtmarine.com> Killed
merged [FIX] dark mode: readable pivot/graph, dark spreadsheet and dashboard chrome, calmer Discuss seams [FIX] viin_backend_theme: dedupe active marker in Appearance menu The Appearance systray menu's active Theme row and active Density row each drew two selected-markers: core's own `.dropdown-item.active::before` FontAwesome checkmark painted on top of this component's own right-aligned check icon, so the active choice showed a double mark instead of one. Suppressed core's marker on both DropdownItem loops with core's own opt-out class `dropdown-item_active_noarrow`, keeping the component's own check as the single indicator. mail's action list already reuses this same class for the identical situation, so this reuses an existing core lever instead of inventing new CSS. Added a tour-driven HttpCase asserting the rendered result - the active row's computed `::before` is 'none' and exactly one visible `.fa-check` remains inside it - in both light and dark colour scheme, so the contract is pinned on render output rather than on markup. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed
merged Fwd from 18 to 19 20260912 01 Merge remote-tracking branch 'Viindoo/18.0' into 19.0 Killed
merged [FIX] viin_brand_mail: fix chatter contrast in light and dark schemes [FIX] viin_brand_mail: fix chatter contrast, harden dark edited marker DARK: the mail chatter embedded in the Odoo 19 spreadsheet editor's comments side panel was unreadable. Odoo keeps the spreadsheet a light island inside a dark app (its dark-bundle-only o_spreadsheet_extended.dark.scss forces .bg-white back to white and pins .btn to a dark label), but core repaints only the button label, not the fill, leaving half of every pair in the wrong colour scheme. Finish that light island for the embedded chatter instead of darkening it, scoped entirely under .o-spreadsheet. Measured live at 1440px in dark mode, before -> after: Log note 1.19 -> 7.92, Activity 1.19 -> 7.92, Send message 2.17 -> 8.24, author name 2.63 -> 10.31, timestamp 2.59 -> 6.11, message body -> 9.25. The same two message nodes the island fix above touches - the empty-message placeholder (3.37:1 worst) and the (edited) marker (4.04:1 worst) - were unreadable everywhere OUTSIDE that island too, because core dims both a second time on top of a tier already tuned to clear AA once. Fix them product-wide instead of leaving the island as the one readable spot: every form-view chatter, the Discuss thread canvas and every chat window, on the panel and on all three message-bubble tints. With that rule in place the island's own placeholder correction became a pure duplicate of it and was dropped. The island's (edited) rule is kept and hardened with its own colour, because the product-wide fix lands that marker on the dark metadata tier, which measures 2.34-2.59:1 on the island's light bubbles - too dark for that ground. LIGHT: the mail message muted tier renders below AA wherever it lands - 4.06:1 on a message bubble, 4.20:1 on the chatter panel - and two nodes are dimmed further by opacity utilities to 2.7:1 and 2.4:1. The shared $text-muted token is deliberately left untouched: it also paints off/disabled affordances that WCAG exempts, and repointing it would change roughly 380 nodes across 205 templates. Re-point the tier for mail message surfaces only. After: 5.48-6.11 across the panel and all bubble variants. Dark stays provably unaffected, since the light rules compile to zero bytes in the dark bundle. Also drop a rule that went dead once the comments panel lost its background utility - it was this module's only reference to another repo's class. A comment on the read-conversation muted tier said its --secondary-color token "does not flip" in dark - true only while $o-main-color-muted and $body-secondary-color were two independent Sass values. dark_palette.scss has since aliased the former onto the latter, so both names now resolve to the one tier this rule already reads: the rule was right, the explanation had gone stale. Reworded to name the tier through its own alias chain instead of restating a divergence that no longer exists. Guard every case above with tests that assert computed WCAG ratios, never colour values. DARK EDITED MARKER, HARDENED: the product-wide dark rule just added above (.o-mail-Message-edited .opacity-50 in mail_dark.scss) read the bare Bootstrap Sass global $text-muted, which has no !default guard, so any module sharing the web.assets_web_dark compile can reassign it from under this one. Core's hr_skills does exactly that. hr_skills/__manifest__.py:53 wildcard-globs 'hr_skills/static/src/scss/*.scss' into web.assets_backend, separately from the intended web.report_assets_pdf entry on line 68; that glob also catches report_employee_cv.scss, whose line 3 hard-assigns $text-muted: #3b4757 with no !default; and the bundle chain web.assets_web_dark -> web.assets_web -> web.assets_backend carries that value in. So whenever hr_skills is installed alongside the branding modules (always true on Runbot, which installs the whole repo) the marker compiled to the wrong colour and failed WCAG AA contrast (~1.3-2.0:1, needs >= 4.5:1). A one-variable install-topology experiment proved it: same commit, same suite - the three branding modules installed alone compiled $text-muted to #8EA5A8 and passed all 147 tests; adding hr_skills to the install set, nothing else changed, compiled it to #3b4757 and failed 12 subtests. That asymmetry is exactly why the branch stayed green on a local, branding-only run while Runbot - which always installs the whole repo - was red on the same code. Re-point the rule to var(--secondary-color) - the CSS custom property Bootstrap emits from $body-secondary-color (a different Sass variable, hard-assigned in dark_palette.scss:76 with no !default, which hr_skills' write never touches) - matching an existing pattern already used twice in this file (:56 and :245) for the analogous chatter-timestamp / notification-item cases. Updated the neighbouring comment to match, since it previously explained the value in terms of $text-muted. Added a regression guard, test_dark_edited_marker_stays_readable_when_another_module_hijacks_text_muted, that splices hr_skills' exact clobbering literal into the real web.assets_web_dark bundle source and recompiles through Odoo's own AssetsBundle.compile_css / ScssStylesheetAsset.compile (libsass), proving the rule survives a hijacked $text-muted regardless of which modules happen to be installed on whatever DB runs this module's own suite (no cr.commit(), no real hr_skills install needed). Verified RED before (12 genuine assertion failures on a live instance with hr_skills installed) and GREEN after (0 failed, 0 errors across 187 tests in viin_backend_theme + viin_brand_web + viin_brand_mail, same instance, hr_skills genuinely installed). Also fixes a flake8 E303 (too many blank lines) left behind while extending the compiled-CSS test suite above. Signed-off-by: David Tran <david.tran@tvtmarine.com> Killed
merged [FIX] viin_brand_mail: fix chatter contrast in light and dark schemes [FIX] viin_brand_mail: fix chatter contrast in light and dark schemes DARK: the mail chatter embedded in the Odoo 19 spreadsheet editor's comments side panel was unreadable. Odoo keeps the spreadsheet a light island inside a dark app (its dark-bundle-only o_spreadsheet_extended.dark.scss forces .bg-white back to white and pins .btn to a dark label), but core repaints only the button label, not the fill, leaving half of every pair in the wrong colour scheme. Finish that light island for the embedded chatter instead of darkening it, scoped entirely under .o-spreadsheet. Measured live at 1440px in dark mode, before -> after: Log note 1.19 -> 7.92, Activity 1.19 -> 7.92, Send message 2.17 -> 8.24, author name 2.63 -> 10.31, timestamp 2.59 -> 6.11, message body -> 9.25. The same two message nodes the island fix above touches - the empty-message placeholder (3.37:1 worst) and the (edited) marker (4.04:1 worst) - were unreadable everywhere OUTSIDE that island too, because core dims both a second time on top of a tier already tuned to clear AA once. Fix them product-wide instead of leaving the island as the one readable spot: every form-view chatter, the Discuss thread canvas and every chat window, on the panel and on all three message-bubble tints. With that rule in place the island's own placeholder correction became a pure duplicate of it and was dropped. The island's (edited) rule is kept and hardened with its own colour, because the product-wide fix lands that marker on the dark metadata tier, which measures 2.34-2.59:1 on the island's light bubbles - too dark for that ground. LIGHT: the mail message muted tier renders below AA wherever it lands - 4.06:1 on a message bubble, 4.20:1 on the chatter panel - and two nodes are dimmed further by opacity utilities to 2.7:1 and 2.4:1. The shared $text-muted token is deliberately left untouched: it also paints off/disabled affordances that WCAG exempts, and repointing it would change roughly 380 nodes across 205 templates. Re-point the tier for mail message surfaces only. After: 5.48-6.11 across the panel and all bubble variants. Dark stays provably unaffected, since the light rules compile to zero bytes in the dark bundle. Also drop a rule that went dead once the comments panel lost its background utility - it was this module's only reference to another repo's class. A comment on the read-conversation muted tier said its --secondary-color token "does not flip" in dark - true only while $o-main-color-muted and $body-secondary-color were two independent Sass values. dark_palette.scss has since aliased the former onto the latter, so both names now resolve to the one tier this rule already reads: the rule was right, the explanation had gone stale. Reworded to name the tier through its own alias chain instead of restating a divergence that no longer exists. Guard every case above with tests that assert computed WCAG ratios, never colour values. Signed-off-by: David Tran <david.tran@tvtmarine.com> Failed