diff --git a/CHANGELOG.md b/CHANGELOG.md
index 257537e..cbdc8f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,132 +1,60 @@
# Changelog
-## [1.1.4] - 2026-03-28
+## [1.2.0] - 2026-03-30
-### 🐛 Bug Fixes
+### ✨ New Features
-- **`display:none` not respected in renderer** (`render_hyper_box_layout.dart`): Added early-return guard in `_tokenizeNode` — elements with `display:none` no longer produce any layout fragments and are correctly hidden. Previously, elements styled with `display:none` (e.g. Wikipedia `[edit]` section links) were still rendered.
+- **Multi-tier Plugin API** (`hyper_render_core`): Third-party packages can now render arbitrary HTML tags as custom Flutter widgets via `HyperNodePlugin` / `HyperPluginRegistry`.
+ - **Block tier** (`isInline == false`, default): widget takes full available width with CSS margins.
+ - **Inline tier** (`isInline == true`): widget flows inside text lines; intrinsic size measured in `performLayout` via `getMaxIntrinsicWidth / getMinIntrinsicHeight`.
+ - Register at startup: `HyperPluginRegistry()..register(MyPlugin())` and pass to `HyperViewer(pluginRegistry: ...)`.
-- **`
` rendered as line break** (`html_adapter.dart`): `
` now correctly returns a styled `BlockNode` with a top border (`borderColor: #CCCCCC, borderWidth: 1px`), matching browser behavior. Previously it was incorrectly treated identically to `
`.
+- **Dirty-flag incremental layout** (`hyper_viewer.dart`): Only re-layout sections whose content changed. Each `DocumentNode` chunk is fingerprinted with `Object.hashAll` over child `textContent`; unchanged sections are reused on the next parse, and `ValueKey(hash)` on `RepaintBoundary` lets Flutter skip re-layout and repaint entirely. ~90% layout rebuild reduction for live-updating feeds.
-- **Whitespace-only space nodes dropped between inline elements** (`html_adapter.dart`): Text nodes consisting only of horizontal spaces (e.g. `" "` between `text more`) were being silently dropped by `.trim().isEmpty`, causing missing word-separating spaces. Fixed to only drop nodes that contain newlines (structural indentation whitespace), not pure-space nodes.
+- **Paged mode** (`HyperRenderMode.paged`): `PageView.builder`-based rendering, one document chunk per page. Suitable for e-book / epub / reader UIs.
+ - Supply a `HyperPageController` for programmatic navigation (`animateToPage`, `nextPage`, `previousPage`, `jumpToPage`) and `ValueNotifier currentPage` for reactive page indicators.
-- **`TextPainter` cache hash collision** (`render_hyper_box.dart`): The `_LruCache` key was computed with `Object.hash()` which can collide for large documents with many distinct text styles, leading to wrong text metrics and subtle layout glitches. Replaced with a new `_TextPainterKey` class using full value equality over all 9 style fields.
+### ♿ Accessibility (WCAG 2.1 AA)
-- **`@override` analyzer warning** (`packages/hyper_render_html/lib/src/css_parser.dart`): Removed incorrect `@override` on `parseKeyframes()`. The `override_on_non_overriding_member` lint flagged this because the parent method has a concrete default body. Now `flutter analyze` reports 0 issues.
+- **Image alt-text semantic nodes** (`render_hyper_box_accessibility.dart`): `
` elements now produce a discrete `SemanticsNode` at the image's layout rect. Screen-reader users can navigate to images element-by-element (WCAG 1.1.1 Non-text Content). Previously alt text only appeared in the flat document-level label.
+- **`aria-label` on links honored** (`render_hyper_box_accessibility.dart`): If an `` element carries an `aria-label` attribute, that value is used as the link's semantic label instead of its text content (WCAG 4.1.2 Name, Role, Value).
-### 📸 Assets & Documentation
+### 🏗️ Refactor — Dead-code elimination
-- **Added `assets/logo.svg`** — vector HyperRender logo now correctly displayed in README header and pub.dev listing.
-- **README**: Fixed broken in-page navigation links (`Quick Start`, `Why Switch?`, `API`, `Packages`) — added emoji prefixes to section headings which generate the leading `-` in GitHub anchor IDs. Improved bottom section with star call-to-action, Discussions, API docs links. Updated version badge to `1.1.4`.
+- **Removed 31 duplicate files from root `lib/src/`** that were identical or outdated copies of the canonical implementations in `packages/hyper_render_core`. Root `lib/src/` now contains only the 17 files that are genuinely unique to the root package (parsers, sanitizer, `HyperViewer`, virtualized selection, `capture_extension`).
+- **`LazyImageQueue` singleton deduplication**: `lib/src/core/lazy_image_queue.dart` was a separate implementation that created a second `LazyImageQueue.instance` — meaning `LazyImageQueue.instance.cancel()` called from outside `HyperViewer` hit a different singleton than the one `HyperViewer` used internally. Root now re-exports `LazyImageQueue` directly from `hyper_render_core` (single shared instance).
+- **Added missing v1.2.0 symbols to root re-export**: `HyperRenderConfig`, `LazyImageQueue`, `HyperNodePlugin`, `HyperPluginRegistry`, `HyperPluginBuildContext`, `LoadingSkeleton`, `HyperErrorWidget`, `FloatCarryover` are now all accessible from `package:hyper_render`.
+- **Consolidated double export**: The redundant second `export 'package:hyper_render_core' show HyperRenderConfig'` line was folded into the main re-export block.
-## [1.1.3] - 2026-03-25
+### 🐛 Bug Fixes
-- Remove `publish_to: none` from all sub-package pubspec.yaml files so pub.dev can verify repository URLs (fixes pub points deduction).
-- Commit DevTools extension web build to git so it is always available without a local rebuild step.
-- Fix `.gitignore` to untrack all sub-package `pubspec.lock` files per Dart library conventions.
-- Fix all unnecessary Flutter SDK import warnings (`rendering.dart`, `services.dart`, `gestures.dart` redundant with broader imports) across `lib/` and `packages/hyper_render_core/lib/` — resolves 6 static-analysis INFO issues and unblocks CI.
+- **Copy action produced empty clipboard** (`virtualized_selection_overlay.dart`, `hyper_selection_overlay.dart`): The `Listener.onPointerDown` callback cleared the active selection before the Copy button's `onPressed` could fire, so `Clipboard.setData` received an empty string. Fixed by guarding `clearSelection()` behind a `_showMenu` / `_showContextMenu` check (matching the pattern already used in the non-virtualized overlay).
+- **Context menu outside hit-testable bounds** (`hyper_selection_overlay.dart`, `virtualized_selection_overlay.dart`): When a selection was near the top of the widget the computed `top` for the `Positioned` menu went negative. `Stack(clipBehavior: Clip.none)` allows visual overflow but Flutter hit-testing is still bounded by the parent — the Copy button was unreachable. Fixed by clamping the top offset: `.clamp(0.0, double.infinity)`.
-All notable changes to HyperRender will be documented in this file.
+- **Scroll vs. text-selection conflict** (`render_hyper_box.dart`): `handleEvent(PointerMoveEvent)` bypassed the gesture arena and fired on every pointer move, creating accidental selections during scrolling. Removed raw-event selection tracking and moved selection initiation to a `LongPressGestureRecognizer` at the widget layer — this correctly competes with the parent scroll view's `VerticalDragGestureRecognizer`, so a quick swipe scrolls while a 500 ms hold begins a text selection (matching iOS/Android native behaviour).
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+- **Virtualized copy menu never appeared** (`virtualized_selection_overlay.dart`): Per-chunk `RenderHyperBox._selection` was set by the old pointer-event tracking, but `VirtualizedSelectionController` (cross-chunk selection) was never populated, so `hasSelection` remained `false` and the menu was never shown. Fixed by routing the long-press start through `VirtualizedSelectionController.startSelection()`.
-## [1.1.2] - 2026-03-25
+- **Selection Escape key fix** (`hyper_selection_overlay.dart`): `Escape` key failed to clear selection because the internal `FocusNode` wasn't reliably focused after selection was established. Fixed by calling `_focusNode.requestFocus()` inside `startSelectionAt`.
-### ✨ New Features
-- **CSS @keyframes** (`DefaultCssParser.parseKeyframes`): `@keyframes` blocks are now parsed from `