diff --git a/.env b/.env
index 22271a91bc1..df60c8ec0b8 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,6 @@
 # Production Build
-BUILD_GRID_VERSION=35.1.0-beta.20260320.1044
-BUILD_CHARTS_VERSION=13.1.0-beta.20260320
+BUILD_GRID_VERSION=35.2.0-beta.20260324.1227
+BUILD_CHARTS_VERSION=13.2.0-beta.20260324
 ENV=local
 NX_BATCH_MODE=true
 NX_ADD_PLUGINS=false
diff --git a/.rulesync/rules/browser-support.md b/.rulesync/rules/browser-support.md
new file mode 120000
index 00000000000..54789643bbf
--- /dev/null
+++ b/.rulesync/rules/browser-support.md
@@ -0,0 +1 @@
+../../external/ag-shared/prompts/guides/browser-support.md
\ No newline at end of file
diff --git a/.rulesync/skills/run-gha-locally b/.rulesync/skills/run-gha-locally
new file mode 120000
index 00000000000..e5a5c995d35
--- /dev/null
+++ b/.rulesync/skills/run-gha-locally
@@ -0,0 +1 @@
+../../external/ag-shared/prompts/skills/run-gha-locally/
\ No newline at end of file
diff --git a/.run/BigInt support.run.xml b/.run/BigInt support.run.xml
deleted file mode 100644
index 1696dcfaa07..00000000000
--- a/.run/BigInt support.run.xml	
+++ /dev/null
@@ -1,5 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="BigInt support" type="JavascriptDebugType" engineId="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" uri="https://plnkr.co/edit/PRzRdjaX4KUkiGPm?open=main.js">
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/.run/Column Tool Panel Icon bug.run.xml b/.run/Column Tool Panel Icon bug.run.xml
new file mode 100644
index 00000000000..c192fe9ad77
--- /dev/null
+++ b/.run/Column Tool Panel Icon bug.run.xml	
@@ -0,0 +1,5 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Column Tool Panel Icon bug" type="JavascriptDebugType" engineId="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" uri="https://plnkr.co/edit/Kh56rmKKhKWuUmYB?open=main.js">
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/.run/Mini filter value formatter.run.xml b/.run/Mini filter value formatter.run.xml
deleted file mode 100644
index de16c6889ef..00000000000
--- a/.run/Mini filter value formatter.run.xml	
+++ /dev/null
@@ -1,5 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Mini filter value formatter" type="JavascriptDebugType" engineId="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" uri="https://plnkr.co/edit/4MC5MKLtNjTb0bEG?open=main.js">
-    <method v="2" />
-  </configuration>
-</component>
\ No newline at end of file
diff --git a/community-modules/locale/package.json b/community-modules/locale/package.json
index b6297b7b523..5c3f5814741 100644
--- a/community-modules/locale/package.json
+++ b/community-modules/locale/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ag-grid-community/locale",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "Localisation Module for AG Grid, providing translations in 31 languages.",
   "main": "./dist/package/main.cjs.js",
   "types": "./dist/types/src/main.d.ts",
diff --git a/community-modules/styles/package.json b/community-modules/styles/package.json
index 7334e563630..254c250603f 100644
--- a/community-modules/styles/package.json
+++ b/community-modules/styles/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ag-grid-community/styles",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "AG Grid Styles and Themes",
   "main": "_index.scss",
   "files": [
diff --git a/documentation/ag-grid-docs/package.json b/documentation/ag-grid-docs/package.json
index 353812b2106..02b02ca097f 100644
--- a/documentation/ag-grid-docs/package.json
+++ b/documentation/ag-grid-docs/package.json
@@ -2,7 +2,7 @@
   "name": "ag-grid-docs",
   "description": "Documentation for AG Grid",
   "type": "module",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "repository": {
     "type": "git",
     "url": "https://github.com/ag-grid/ag-grid.git"
@@ -53,17 +53,17 @@
     "@types/react": "^18.2.47",
     "@types/react-dom": "^18.2.18",
     "@pqina/flip": "^1.8.4",
-    "ag-charts-angular": "13.1.0-beta.20260320",
-    "ag-charts-community": "13.1.0-beta.20260320",
-    "ag-charts-enterprise": "13.1.0-beta.20260320",
-    "ag-charts-types": "13.1.0-beta.20260320",
-    "ag-charts-react": "13.1.0-beta.20260320",
-    "ag-charts-vue3": "13.1.0-beta.20260320",
-    "ag-grid-angular": "35.1.0-beta.20260320.1044",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
-    "ag-grid-enterprise": "35.1.0-beta.20260320.1044",
-    "ag-grid-react": "35.1.0-beta.20260320.1044",
-    "ag-grid-vue3": "35.1.0-beta.20260320.1044",
+    "ag-charts-angular": "13.2.0-beta.20260324",
+    "ag-charts-community": "13.2.0-beta.20260324",
+    "ag-charts-enterprise": "13.2.0-beta.20260324",
+    "ag-charts-types": "13.2.0-beta.20260324",
+    "ag-charts-react": "13.2.0-beta.20260324",
+    "ag-charts-vue3": "13.2.0-beta.20260324",
+    "ag-grid-angular": "35.2.0-beta.20260324.1227",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
+    "ag-grid-enterprise": "35.2.0-beta.20260324.1227",
+    "ag-grid-react": "35.2.0-beta.20260324.1227",
+    "ag-grid-vue3": "35.2.0-beta.20260324.1227",
     "algoliasearch": "^4.18.0",
     "astro": "5.16.6",
     "cheerio": "^1.0.0",
diff --git a/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json b/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
index 50d52160310..41081a936a0 100644
--- a/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
+++ b/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
@@ -620,7 +620,7 @@
         },
         "getStructuredSchema": {
             "more": {
-                "name": "CSV Export",
+                "name": "AI Toolkit",
                 "url": "./ai-toolkit/"
             }
         }
diff --git a/documentation/ag-grid-docs/src/content/docs/angular-ngzone/index.mdoc b/documentation/ag-grid-docs/src/content/docs/angular-ngzone/index.mdoc
index 7ac16d03d07..b86203b43be 100644
--- a/documentation/ag-grid-docs/src/content/docs/angular-ngzone/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/angular-ngzone/index.mdoc
@@ -5,6 +5,10 @@ frameworks: ["angular"]
 
 Recommended NgZoneOptions and details of AG Grid's performance optimisations to avoid [Zone Pollution](https://angular.dev/best-practices/zone-pollution).
 
+{% note %}
+From Angular 21, new applications are zoneless by default. The optimisations on this page only apply to Zone based applications. See [Angular Zoneless](https://angular.dev/guide/zoneless) for more details.
+{% /note %}
+
 ## Recommended NgZoneOptions
 
 Angular supports coalescing of change detection cycles via [NgZoneOptions](https://angular.dev/api/core/NgZoneOptions). This can be set via `provideZoneChangeDetection` when bootstrapping your application. We recommend enabling `eventCoalescing` and `runCoalescing` for optimal AG Grid performance.
diff --git a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phone-editor.component_angular.ts b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phone-editor.component_angular.ts
index 70e426b393a..ca263080b21 100644
--- a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phone-editor.component_angular.ts
+++ b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phone-editor.component_angular.ts
@@ -58,7 +58,7 @@ export class PhoneEditor implements ICellEditorAngularComp, AfterViewInit {
 
             const { cellStartedEdit, eventKey } = this.params;
 
-            if (cellStartedEdit && eventKey.length === 1) {
+            if (cellStartedEdit && eventKey?.length === 1) {
                 this.inputRef.nativeElement.value = eventKey;
             }
         });
diff --git a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_typescript.ts b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_typescript.ts
index a89eda77f2e..a4749eab92a 100644
--- a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_typescript.ts
+++ b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_typescript.ts
@@ -35,7 +35,7 @@ export class PhoneEditor implements ICellEditorComp<string> {
 
         const { cellStartedEdit, eventKey } = this.params;
 
-        if (cellStartedEdit && eventKey.length === 1) {
+        if (cellStartedEdit && eventKey?.length === 1) {
             this.eInput.value = eventKey;
         }
     }
diff --git a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_vue3.ts b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_vue3.ts
index 7b3d91b8f08..efa1ac17389 100644
--- a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_vue3.ts
+++ b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/phoneEditor_vue3.ts
@@ -34,7 +34,7 @@ export default {
 
                 const { cellStartedEdit, eventKey } = this.params;
 
-                if (cellStartedEdit && eventKey.length === 1) {
+                if (cellStartedEdit && eventKey?.length === 1) {
                     this.$refs.input.value = eventKey;
                 }
             });
diff --git a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/provided/reactFunctionalTs/phoneEditor.tsx b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/provided/reactFunctionalTs/phoneEditor.tsx
index 5c6efcc2a03..ca958227f9f 100644
--- a/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/provided/reactFunctionalTs/phoneEditor.tsx
+++ b/documentation/ag-grid-docs/src/content/docs/cell-editing-validation/_examples/component-editor-validation/provided/reactFunctionalTs/phoneEditor.tsx
@@ -27,7 +27,7 @@ export default memo(({ value, onValueChange, validate, cellStartedEdit, eventKey
         inputRef.current?.focus();
         inputRef.current?.select();
 
-        if (cellStartedEdit && eventKey.length === 1) {
+        if (cellStartedEdit && eventKey?.length === 1) {
             setInternalValue(eventKey);
         }
     }, []);
diff --git a/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-delta-aggregation/main.ts b/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-delta-aggregation/main.ts
index cef08fff9a9..003e396b880 100644
--- a/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-delta-aggregation/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-delta-aggregation/main.ts
@@ -44,7 +44,6 @@ const gridOptions: GridOptions = {
             headerName: 'Total',
             type: 'totalColumn',
             minWidth: 120,
-            aggFunc: 'sum',
             valueGetter: 'getValue("a") + getValue("b") + getValue("c") + getValue("d")',
         },
     ],
diff --git a/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-filter-sort-group/main.ts b/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-filter-sort-group/main.ts
index 6bb17ae569f..42dd042f194 100644
--- a/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-filter-sort-group/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-filter-sort-group/main.ts
@@ -39,7 +39,6 @@ const gridOptions: GridOptions = {
         {
             headerName: 'Total',
             type: 'totalColumn',
-            aggFunc: 'sum',
             valueGetter: 'getValue("a") + getValue("b") + getValue("c") + getValue("d")',
         },
     ],
@@ -68,9 +67,16 @@ const gridOptions: GridOptions = {
     groupDefaultExpanded: 1,
     suppressAggFuncInHeader: true,
     allowShowChangeAfterFilter: true,
-    refreshAfterGroupEdit: true,
+    onCellValueChanged: onCellValueChanged,
 };
 
+function onCellValueChanged(params: CellValueChangedEvent) {
+    const data = params.data;
+    if (data) {
+        params.api.applyTransaction({ update: [data] });
+    }
+}
+
 function getRowData() {
     const rowData = [];
     for (let i = 1; i <= 10; i++) {
diff --git a/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-groups/main.ts b/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-groups/main.ts
index 5b5fbeb77de..a16a08d6032 100644
--- a/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-groups/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/change-detection/_examples/change-detection-groups/main.ts
@@ -37,7 +37,6 @@ const gridOptions: GridOptions = {
         {
             headerName: 'Total',
             type: 'totalColumn',
-            aggFunc: 'sum',
             valueGetter: 'getValue("a") + getValue("b") + getValue("c") + getValue("d")',
         },
     ],
diff --git a/documentation/ag-grid-docs/src/content/docs/change-detection/index.mdoc b/documentation/ag-grid-docs/src/content/docs/change-detection/index.mdoc
index ae3116a6760..d51df5039e4 100644
--- a/documentation/ag-grid-docs/src/content/docs/change-detection/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/change-detection/index.mdoc
@@ -109,7 +109,7 @@ The example below shows change detection impacting the result of groups. The gri
 
 -   Column 'Group' is marked as a [Row Group](./grouping/) and columns A to D are marked as [Aggregation](./aggregation/) columns so that their values are summed into the group level.
 
--   Column 'Total' has a valueGetter which gives a sum of all columns A to D for the rows, and a `sum` aggregation for the groups.
+-   Column 'Total' has a valueGetter which gives a sum of all columns A to D.
 
 -   Columns A to D are editable. If you edit a cells value, then the aggregate value at the group level is also updated to reflect the change. This is because the grid is recalculating the aggregations as a result of the change.
 
@@ -165,14 +165,7 @@ For this reason, if you want to update the sorting, filtering or group grouping
 
 ### Example: Change Detection and Filter / Sort / Group
 
-The following example is the same as the example above [Change Detection and Groups](#example-change-detection-groups) except it sets the flag `refreshAfterGroupEdit` to true.
-This flag tells the grid to refresh the grouping, sorting and filtering if the group column changes.
-
-See [Editing Groups](./grouping-edit/) for more on editing grouped data and `refreshAfterGroupEdit`.
-
-{% gridExampleRunner title="Change Detection with Filter / Sort / Group" name="change-detection-filter-sort-group" /%}
-
-From the example, the following can be noted:
+The following example is the same as the example above [Change Detection and Groups](#example-change-detection-groups) except it gets the grid to do a transaction update so that the grouping, sorting and filtering are recomputed. From the example, the following can be noted:
 
 -   As before, updating any value will update the total column and aggregated group columns.
 
@@ -182,6 +175,8 @@ From the example, the following can be noted:
 
 -   If you set a filter (eg filter 'A' to be 'less than 50') and then change the data so that a row no longer passes the filter, the grid will fix itself so that the filtering is maintained. In other words, the updated row will be removed if it no longer passes the filter.
 
+{% gridExampleRunner title="Change Detection with Filter / Sort / Group" name="change-detection-filter-sort-group" /%}
+
 ## Aggregation Path Selection
 
 When data in the grid updates and aggregations are active, the grid will not recompute all aggregations again from scratch. Instead it will be selective as to what aggregations need to be re-computed.
diff --git a/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/example.spec.ts
new file mode 100644
index 00000000000..ed79767b830
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/example.spec.ts
@@ -0,0 +1,11 @@
+import { clickAllButtons, ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils';
+
+test.agExample(import.meta, () => {
+    test.eachFramework('Example', async ({ page }) => {
+        // PLACEHOLDER - MINIMAL TEST TO ENSURE GRID LOADS WITHOUT ERRORS
+        await ensureGridReady(page);
+        await waitForGridContent(page);
+        await clickAllButtons(page);
+        // END PLACEHOLDER
+    });
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/index.html b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/index.html
new file mode 100644
index 00000000000..378fad58398
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/index.html
@@ -0,0 +1 @@
+<div id="myGrid" style="height: 100%"></div>
diff --git a/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/main.ts b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/main.ts
new file mode 100644
index 00000000000..986450ee8f9
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range-with-headers/main.ts
@@ -0,0 +1,57 @@
+import type { GridApi, GridOptions } from 'ag-grid-community';
+import {
+    ClientSideRowModelModule,
+    ModuleRegistry,
+    NumberEditorModule,
+    TextEditorModule,
+    ValidationModule,
+    createGrid,
+} from 'ag-grid-community';
+import { CellSelectionModule, ClipboardModule, ColumnMenuModule, ContextMenuModule } from 'ag-grid-enterprise';
+
+ModuleRegistry.registerModules([
+    NumberEditorModule,
+    TextEditorModule,
+    ClientSideRowModelModule,
+    ClipboardModule,
+    ColumnMenuModule,
+    ContextMenuModule,
+    CellSelectionModule,
+    ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
+]);
+
+let gridApi: GridApi<IOlympicData>;
+
+const gridOptions: GridOptions<IOlympicData> = {
+    columnDefs: [
+        { field: 'athlete', minWidth: 200 },
+        { field: 'age' },
+        { field: 'country', minWidth: 150 },
+        { field: 'year' },
+        { field: 'date', minWidth: 150 },
+        { field: 'sport', minWidth: 150 },
+        { field: 'gold' },
+        { field: 'silver' },
+        { field: 'bronze' },
+        { field: 'total' },
+    ],
+
+    defaultColDef: {
+        editable: true,
+        flex: 1,
+        minWidth: 100,
+    },
+
+    cellSelection: true,
+    copyHeadersToClipboard: true,
+};
+
+// setup the grid after the page has finished loading
+document.addEventListener('DOMContentLoaded', () => {
+    const gridDiv = document.querySelector<HTMLElement>('#myGrid')!;
+    gridApi = createGrid(gridDiv, gridOptions);
+
+    fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
+        .then((response) => response.json())
+        .then((data: IOlympicData[]) => gridApi!.setGridOption('rowData', data));
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range/main.ts b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range/main.ts
index 986450ee8f9..e9680e2c6e4 100644
--- a/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/clipboard/_examples/copy-range/main.ts
@@ -43,7 +43,6 @@ const gridOptions: GridOptions<IOlympicData> = {
     },
 
     cellSelection: true,
-    copyHeadersToClipboard: true,
 };
 
 // setup the grid after the page has finished loading
diff --git a/documentation/ag-grid-docs/src/content/docs/clipboard/index.mdoc b/documentation/ag-grid-docs/src/content/docs/clipboard/index.mdoc
index 90711b4e3bc..fbb2255de83 100644
--- a/documentation/ag-grid-docs/src/content/docs/clipboard/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/clipboard/index.mdoc
@@ -18,15 +18,11 @@ When [Cell Ranges](./cell-selection) are enabled by setting `gridOptions.cellSel
 
 Multiple cell ranges can be selected at once using {% kbd "^ Ctrl" /%} and dragging with the mouse. When copying, all ranges will be copied to the clipboard. Note that the relative positions of multiple ranges is not preserved when copying, they are stacked vertically in the clipboard.
 
-The column headers can be copied to the clipboard in addition to the cell contents by enabling the option `copyHeadersToClipboard`.
-
-{% apiDocumentation source="grid-options/properties.json" section="clipboard" names=["copyHeadersToClipboard"] /%}
-
-In the below example `copyHeadersToClipboard` has been enabled, try:
+In the below example try:
 
 -   Select a cell range with click & drag
 -   Copy with {% kbd "^ Ctrl" /%}+{% kbd "C" /%}
--   Paste into an external program / text editor, note that the column headers were also copied.
+-   Paste into an external program / text editor.
 
 {% gridExampleRunner title="Copying Cell Ranges" name="copy-range" /%}
 
@@ -54,6 +50,26 @@ You can change this behaviour by toggling the "Copy Selected Rows" checkbox to e
 
 {% gridExampleRunner title="Copying Rows" name="copy-row" /%}
 
+## Copying Headers
+
+The column headers can be copied to the clipboard in addition to the cell contents by enabling the option `copyHeadersToClipboard`.
+
+```{% frameworkTransform=true %}
+const gridOptions = {
+    copyHeadersToClipboard: true,
+}
+```
+
+{% apiDocumentation source="grid-options/properties.json" section="clipboard" names=["copyHeadersToClipboard"] /%}
+
+In the below example try:
+
+-   Select a cell range with click & drag
+-   Copy with {% kbd "^ Ctrl" /%}+{% kbd "C" /%}
+-   Paste into an external program / text editor, note that the column headers were also copied.
+
+{% gridExampleRunner title="Copying Cell Ranges and Headers" name="copy-range-with-headers" /%}
+
 ## Custom Clipboard Interaction
 
 If you want to do the copy to clipboard yourself (i.e. not use the grid's clipboard interaction) then implement the callback `sendToClipboard(params)`. Use this if you are in a non-standard web container that has a bespoke API for interacting with the clipboard. The callback gets the data to go into the clipboard, it's your job to call the bespoke API.
diff --git a/documentation/ag-grid-docs/src/content/docs/components/index.mdoc b/documentation/ag-grid-docs/src/content/docs/components/index.mdoc
index 2909b63dfcd..30361288c1e 100644
--- a/documentation/ag-grid-docs/src/content/docs/components/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/components/index.mdoc
@@ -245,29 +245,29 @@ information between the components.
 
 The below table gives a summary of the components, where they are configured and using what attribute.
 
-| Component                      | Where                                                    | Attribute                                                                        |
-| ------------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------- |
-| Cell Component                 | Column Definition                                        | cellRenderer{% br /%}cellRendererParams{% br /%}cellRendererSelector             |
-| Editor Component               | Column Definition                                        | cellEditor{% br /%}cellEditorParams{% br /%}cellEditorSelector                   |
-| Filter                         | Column Definition                                        | filter{% br /%}filterParams                                                      |
-| Floating Filter                | Column Definition                                        | floatingFilter{% br /%}floatingFilterParams                                      |
-| Date Component                 | Column Definition                                        | dateComponent{% br /%}dateComponentParams                                        |
-| Header Component               | Column Definition                                        | headerComponent{% br /%}headerComponentParams                                    |
-| Inner Header Component         | Column Definition                                        | innerHeaderComponent{% br /%}innerHeaderComponentParams                          |
-| Header Group Component         | Column Definition                                        | headerGroupComponent{% br /%}headerGroupComponentParams                          |
-| Inner Header Group Component   | Column Definition                                        | innerHeaderGroupComponent{% br /%}innerHeaderGroupComponentParams                |
-| Tooltip Component              | Column Definition                                        | tooltipComponent{% br /%}tooltipComponentParams                                  |
-| Group Row Cell Component       | Grid Option                                              | groupRowRenderer{% br /%}groupRowRendererParams                                  |
-| Group Row Inner Cell Component | cellRendererParams{%br /%}groupRowRendererParams{%br /%} | innerRenderer{% br /%}innerRendererParams                                        |
-| Detail Cell Component          | Grid Option                                              | detailCellRenderer{% br /%}detailCellRendererParams                              |
-| Full Width Cell Component      | Grid Option                                              | fullWidthCellRenderer{% br /%}fullWidthCellRendererParams                        |
-| Loading Cell Component         | Grid Option{%br /%}Column Definition                     | loadingCellRenderer{% br /%}loadingCellRendererParams                            |
-| Overlay Component              | Grid Option                                              | overlayComponent{% br /%}overlayComponentParams{% br /%}overlayComponentSelector |
-| Active Overlay                 | Grid Option                                              | activeOverlay{% br /%}activeOverlayParams                                        |
-| Drag and Drop Image            | Grid Option                                              | dragAndDropImageComponent{% br /%}dragAndDropImageComponentParams                |
-| Status Bar Component           | Grid Option -> Status Bar                                | statusPanel{% br /%}statusPanelParams                                            |
-| Tool Panel                     | Grid Option -> Side Bar                                  | toolPanel{% br /%}toolPanelParams                                                |
-| Menu Item                      | Grid Option -> Menu                                      | menuItem{% br /%}menuItemParams                                                  |
+| Component                                                                       | Where                                                    | Attribute                                                                        |
+| ------------------------------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------- |
+| [Cell Component](./component-cell-renderer/)                                    | Column Definition                                        | cellRenderer{% br /%}cellRendererParams{% br /%}cellRendererSelector             |
+| [Editor Component](./cell-editors/)                                             | Column Definition                                        | cellEditor{% br /%}cellEditorParams{% br /%}cellEditorSelector                   |
+| [Filter](./component-filter/)                                                   | Column Definition                                        | filter{% br /%}filterParams                                                      |
+| [Floating Filter](./component-floating-filter/)                                 | Column Definition                                        | floatingFilter{% br /%}floatingFilterParams                                      |
+| [Date Component](./filter-date/#custom-selection-component)                     | Column Definition                                        | dateComponent{% br /%}dateComponentParams                                        |
+| [Header Component](./column-headers-components/#custom-component)               | Column Definition                                        | headerComponent{% br /%}headerComponentParams                                    |
+| [Inner Header Component](./column-headers-components/#inner-header-component)   | Column Definition                                        | innerHeaderComponent{% br /%}innerHeaderComponentParams                          |
+| [Header Group Component](./column-groups/#custom-group-component)               | Column Definition                                        | headerGroupComponent{% br /%}headerGroupComponentParams                          |
+| [Inner Header Group Component](./column-groups/#inner-header-group-component)   | Column Definition                                        | innerHeaderGroupComponent{% br /%}innerHeaderGroupComponentParams                |
+| [Tooltip Component](./tooltips/#custom-component)                               | Column Definition                                        | tooltipComponent{% br /%}tooltipComponentParams                                  |
+| [Group Row Cell Component](./grouping-group-rows/#custom-cell-renderer)         | Grid Option                                              | groupRowRenderer{% br /%}groupRowRendererParams                                  |
+| [Group Row Inner Cell Component](./grouping-group-rows/#custom-inner-renderer)  | cellRendererParams{%br /%}groupRowRendererParams{%br /%} | innerRenderer{% br /%}innerRendererParams                                        |
+| [Detail Cell Component](./master-detail-custom-detail/)                         | Grid Option                                              | detailCellRenderer{% br /%}detailCellRendererParams                              |
+| [Full Width Cell Component](./full-width-rows/#understanding-full-width)        | Grid Option                                              | fullWidthCellRenderer{% br /%}fullWidthCellRendererParams                        |
+| [Loading Cell Component](./component-loading-cell-renderer/)                    | Grid Option{%br /%}Column Definition                     | loadingCellRenderer{% br /%}loadingCellRendererParams                            |
+| [Overlay Component](./overlays-provided/#customisation)                         | Grid Option                                              | overlayComponent{% br /%}overlayComponentParams{% br /%}overlayComponentSelector |
+| [Active Overlay](./overlays-active/)                                            | Grid Option                                              | activeOverlay{% br /%}activeOverlayParams                                        |
+| [Drag and Drop Image](./row-dragging-customisation/#custom-drag-and-drop-image) | Grid Option                                              | dragAndDropImageComponent{% br /%}dragAndDropImageComponentParams                |
+| [Status Bar Component](./status-bar/#custom-panels)                             | Grid Option -> Status Bar                                | statusPanel{% br /%}statusPanelParams                                            |
+| [Tool Panel](./component-tool-panel/)                                           | Grid Option -> Side Bar                                  | toolPanel{% br /%}toolPanelParams                                                |
+| [Menu Item](./component-menu-item/)                                             | Grid Option -> Menu                                      | menuItem{% br /%}menuItemParams                                                  |
 
 ## Grid Provided Components
 
diff --git a/documentation/ag-grid-docs/src/content/docs/example-demos.spec.ts b/documentation/ag-grid-docs/src/content/docs/example-demos.spec.ts
index f5930d78484..ceb684877da 100644
--- a/documentation/ag-grid-docs/src/content/docs/example-demos.spec.ts
+++ b/documentation/ag-grid-docs/src/content/docs/example-demos.spec.ts
@@ -10,6 +10,7 @@ test.describe(`Demo Examples`, async () => {
 
     test.beforeEach(async ({ page }) => {
         errors = setupConsoleExpectations(page);
+        await page.route('**://cdn.cookielaw.org/**', (route) => route.abort());
     });
 
     test.afterEach(async () => {
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-angular.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-angular.mdoc
index 1bd7102f636..73b40a88136 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-angular.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-angular.mdoc
@@ -10,7 +10,7 @@ The api can be accessed from the `AgGridAngular` component via a `@ViewChild` de
 @ViewChild('myGrid') grid!: AgGridAngular;
 
 // Access the api off the grid
-onClick() { this.grid.api.deselectRows() }
+onClick() { this.grid.api.deselectAll() }
 ```
 
 The `api` is also provided on the params for all grid events and callbacks. The first event fired is the `gridReady` event and that can be used to store a reference to the api.
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-javascript.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-javascript.mdoc
index 1686880126b..5da2792f187 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-javascript.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-interface/_api-javascript.mdoc
@@ -16,7 +16,7 @@ Alternatively all Grid [callbacks](./grid-options/) and [events](./grid-events/)
 
 ```js
 const gridOptions: GridOptions = {
-    onGridReady: (event: GridReadyEvent) {
+    onGridReady: (event: GridReadyEvent) => {
         // use api from event
         event.api.ensureIndexVisible(10);
     }
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-angular.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-angular.mdoc
index 93a036be411..a841b7f1b38 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-angular.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-angular.mdoc
@@ -1,5 +1,5 @@
 {% if isFramework("angular") %}
-The grid is configure via Inputs on the `ag-grid-angular` component. Properties consist of simple types, arrays, complex objects and callback functions.
+The grid is configured via Inputs on the `ag-grid-angular` component. Properties consist of simple types, arrays, complex objects and callback functions.
 
 ```js
 <ag-grid-angular
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-react.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-react.mdoc
index bfc47757b39..c74a320774a 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-react.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-react.mdoc
@@ -1,5 +1,5 @@
 {% if isFramework("react") %}
-The grid is configure via props on the `AgGridReact` component. Props consist of simple types, arrays, complex objects and callback functions.
+The grid is configured via props on the `AgGridReact` component. Props consist of simple types, arrays, complex objects and callback functions.
 
 ```jsx
 <AgGridReact
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-vue.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-vue.mdoc
index 3bb0d10a1fa..bb2936d73fe 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-vue.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-interface/_index-vue.mdoc
@@ -1,6 +1,6 @@
 {% if isFramework("vue") %}
 The grid is configure via the `ag-grid-vue` component. Properties consist of simple types, arrays, complex objects and callback functions.
-Properties are registered using their 'dash' syntax and not camel-case. For example, the property `pivotMode` is bound using `pivot-mode`. The following example shows some bindings:
+The following example shows some bindings (demonstrating that both the dash and camel case style is acceptable):
 
 ```jsx
 <ag-grid-vue
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-size/_examples/example2/main.ts b/documentation/ag-grid-docs/src/content/docs/grid-size/_examples/example2/main.ts
index 367ec1c2e6e..5ee4dd679a4 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-size/_examples/example2/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grid-size/_examples/example2/main.ts
@@ -9,7 +9,6 @@ import type {
 import {
     ClientSideRowModelApiModule,
     ClientSideRowModelModule,
-    ColumnAutoSizeModule,
     ModuleRegistry,
     RenderApiModule,
     RowApiModule,
@@ -23,7 +22,6 @@ ModuleRegistry.registerModules([
     ClientSideRowModelApiModule,
     RenderApiModule,
     RowApiModule,
-    ColumnAutoSizeModule,
     ClientSideRowModelModule,
     ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
 ]);
@@ -35,22 +33,19 @@ let gridApi: GridApi;
 
 const gridOptions: GridOptions = {
     columnDefs: [
-        { field: 'athlete', minWidth: 150 },
-        { field: 'age', minWidth: 70, maxWidth: 90 },
-        { field: 'country', minWidth: 130 },
-        { field: 'year', minWidth: 70, maxWidth: 90 },
-        { field: 'date', minWidth: 120 },
-        { field: 'sport', minWidth: 120 },
-        { field: 'gold', minWidth: 80 },
-        { field: 'silver', minWidth: 80 },
-        { field: 'bronze', minWidth: 80 },
-        { field: 'total', minWidth: 80 },
+        { field: 'athlete', width: 140 },
+        { field: 'age', width: 60 },
+        { field: 'country', width: 130 },
+        { field: 'year', width: 70 },
+        { field: 'date', width: 110 },
+        { field: 'sport', width: 110 },
+        { field: 'gold', flex: 1 },
+        { field: 'silver', flex: 1 },
+        { field: 'bronze', flex: 1 },
+        { field: 'total', flex: 1 },
     ],
 
     rowData: getData(),
-    autoSizeStrategy: {
-        type: 'fitGridWidth',
-    },
     onGridReady: (params: GridReadyEvent) => {
         minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
         currentRowHeight = minRowHeight;
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-custom-date-time/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-custom-date-time/main.ts
index fbad2d5c32a..9833223cd7d 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-custom-date-time/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-custom-date-time/main.ts
@@ -35,6 +35,7 @@ const gridOptions: GridOptions<IOlympicData> = {
             enableRowGroup: true,
             enablePivot: true,
             groupHierarchy: ['year', 'week'],
+            minWidth: 120,
         },
         { field: 'country' },
         { field: 'sport' },
@@ -45,7 +46,7 @@ const gridOptions: GridOptions<IOlympicData> = {
         minWidth: 100,
     },
     autoGroupColumnDef: {
-        minWidth: 200,
+        minWidth: 225,
     },
     sideBar: 'columns',
     rowGroupPanelShow: 'always',
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-date-time/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-date-time/main.ts
index fb7cc6bbc0b..e3a159048fa 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-date-time/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-data/_examples/grouping-date-time/main.ts
@@ -27,6 +27,7 @@ const COL_DEFS: ColDef<IOlympicData>[] = [
         enableRowGroup: true,
         enablePivot: true,
         groupHierarchy: ['year', 'month'],
+        minWidth: 120,
     },
     { field: 'country' },
     { field: 'sport' },
@@ -40,7 +41,7 @@ const gridOptions: GridOptions<IOlympicData> = {
         minWidth: 100,
     },
     autoGroupColumnDef: {
-        minWidth: 200,
+        minWidth: 225,
     },
     sideBar: 'columns',
     rowGroupPanelShow: 'always',
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/data.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/data.ts
index 9391ad32060..3e18f8fbb09 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/data.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/data.ts
@@ -5,9 +5,6 @@ export interface MetricsRecord {
     employee: string;
     salary: number;
     bonus: number;
-    rating: number;
-    hours: number;
-    overtime: number;
     projects: number;
     seniority: number;
     score: number;
@@ -23,9 +20,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Alice',
             salary: 90,
             bonus: 15,
-            rating: 4.5,
-            hours: 40,
-            overtime: 5,
             projects: 3,
             seniority: 5,
             score: 88,
@@ -37,9 +31,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Bob',
             salary: 85,
             bonus: 12,
-            rating: 4.2,
-            hours: 38,
-            overtime: 3,
             projects: 2,
             seniority: 3,
             score: 82,
@@ -51,9 +42,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Carol',
             salary: 78,
             bonus: 10,
-            rating: 3.8,
-            hours: 42,
-            overtime: 8,
             projects: 4,
             seniority: 2,
             score: 75,
@@ -67,9 +55,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Dave',
             salary: 95,
             bonus: 18,
-            rating: 4.8,
-            hours: 44,
-            overtime: 10,
             projects: 5,
             seniority: 7,
             score: 92,
@@ -81,9 +66,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Eve',
             salary: 88,
             bonus: 14,
-            rating: 4.0,
-            hours: 40,
-            overtime: 6,
             projects: 3,
             seniority: 4,
             score: 80,
@@ -95,9 +77,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Fiona',
             salary: 82,
             bonus: 11,
-            rating: 3.9,
-            hours: 41,
-            overtime: 7,
             projects: 4,
             seniority: 3,
             score: 77,
@@ -111,9 +90,6 @@ export function getData(): MetricsRecord[] {
             employee: 'George',
             salary: 74,
             bonus: 9,
-            rating: 4.1,
-            hours: 39,
-            overtime: 4,
             projects: 3,
             seniority: 3,
             score: 79,
@@ -125,9 +101,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Hannah',
             salary: 71,
             bonus: 8,
-            rating: 3.6,
-            hours: 40,
-            overtime: 2,
             projects: 2,
             seniority: 2,
             score: 73,
@@ -141,9 +114,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Ivan',
             salary: 72,
             bonus: 20,
-            rating: 4.3,
-            hours: 45,
-            overtime: 12,
             projects: 6,
             seniority: 6,
             score: 85,
@@ -155,9 +125,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Julia',
             salary: 68,
             bonus: 16,
-            rating: 3.9,
-            hours: 42,
-            overtime: 8,
             projects: 4,
             seniority: 3,
             score: 78,
@@ -169,9 +136,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Kevin',
             salary: 75,
             bonus: 22,
-            rating: 4.6,
-            hours: 48,
-            overtime: 15,
             projects: 7,
             seniority: 8,
             score: 90,
@@ -185,9 +149,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Laura',
             salary: 65,
             bonus: 11,
-            rating: 3.7,
-            hours: 40,
-            overtime: 4,
             projects: 3,
             seniority: 2,
             score: 72,
@@ -199,9 +160,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Mike',
             salary: 70,
             bonus: 13,
-            rating: 4.1,
-            hours: 38,
-            overtime: 2,
             projects: 2,
             seniority: 4,
             score: 79,
@@ -215,9 +173,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Nina',
             salary: 77,
             bonus: 14,
-            rating: 4.4,
-            hours: 41,
-            overtime: 6,
             projects: 5,
             seniority: 4,
             score: 86,
@@ -229,9 +184,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Oscar',
             salary: 73,
             bonus: 12,
-            rating: 3.8,
-            hours: 39,
-            overtime: 3,
             projects: 3,
             seniority: 2,
             score: 76,
@@ -243,9 +195,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Paula',
             salary: 80,
             bonus: 16,
-            rating: 4.2,
-            hours: 43,
-            overtime: 9,
             projects: 4,
             seniority: 5,
             score: 83,
@@ -259,9 +208,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Quinn',
             salary: 69,
             bonus: 10,
-            rating: 3.5,
-            hours: 37,
-            overtime: 1,
             projects: 2,
             seniority: 1,
             score: 70,
@@ -273,9 +219,6 @@ export function getData(): MetricsRecord[] {
             employee: 'Rachel',
             salary: 76,
             bonus: 15,
-            rating: 4.0,
-            hours: 40,
-            overtime: 5,
             projects: 3,
             seniority: 3,
             score: 81,
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/main.ts
index e542b10aa73..c60fa719613 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/distribute-group-value-all-agg/main.ts
@@ -63,6 +63,36 @@ const sumOfSquaresValueSetter: GroupRowValueSetterFunc<MetricsRecord> = ({ newVa
     return changed;
 };
 
+/**
+ * Cross-column groupRowValueSetter: editing the group row's "Rate %"
+ * sets each leaf child's bonus to a percentage of their individual salary.
+ *
+ * Uses getAggregatedChildren with recursive=true to reach all descendant
+ * leaf rows, since intermediate group rows don't have salary data.
+ *
+ * For example, entering 20 on the Engineering group sets each engineer's
+ * bonus to 20% of their salary: Alice (salary 90) gets bonus 18,
+ * Dave (salary 95) gets bonus 19, etc.
+ */
+const bonusRateSetter: GroupRowValueSetterFunc<MetricsRecord> = ({ newValue, node, column }) => {
+    const rate = Number(newValue) / 100;
+    if (!Number.isFinite(rate)) {
+        return false;
+    }
+    const leaves = node.getAggregatedChildren(column, true);
+    if (!leaves.length) {
+        return false;
+    }
+    let changed = false;
+    for (const child of leaves) {
+        const salary = child.data?.salary ?? 0;
+        if (child.setDataValue('bonus', Math.round(salary * rate), 'data')) {
+            changed = true;
+        }
+    }
+    return changed;
+};
+
 const gridOptions: GridOptions<MetricsRecord> = {
     columnDefs: [
         { field: 'department', rowGroup: true, hide: true },
@@ -76,27 +106,33 @@ const gridOptions: GridOptions<MetricsRecord> = {
             groupRowValueSetter: { precision: 0 },
         },
 
-        // avg: overwrites every child with the edited value
-        { field: 'bonus', aggFunc: 'avg' },
-
-        // min: writes to the child currently holding the minimum
-        { field: 'rating', aggFunc: 'min' },
-
-        // max: writes to the child currently holding the maximum
-        { field: 'hours', aggFunc: 'max' },
-
-        // count: overwrites every child
-        { field: 'overtime', aggFunc: 'count' },
+        // avg: default strategy is 'overwrite' — sets every child to the edited value
+        {
+            field: 'bonus',
+            aggFunc: 'avg',
+            valueFormatter: ({ value }) => (value != null ? Number(value).toFixed(2) : ''),
+        },
 
-        // first: writes to the first child only
-        { field: 'projects', aggFunc: 'first' },
+        // No aggFunc: the default strategy is 'overwrite', writing the edited
+        // value to every child. The group cell is blank (no aggregation) but
+        // still editable via groupRowEditable.
+        { field: 'projects' },
 
-        // last: writes to the last child only
-        { field: 'seniority', aggFunc: 'last' },
+        // Cross-column custom callback: editing the group row's "Rate"
+        // reads each child's salary and writes a computed bonus.
+        {
+            headerName: 'Rate %',
+            valueGetter: ({ data }) => (data ? (data.bonus / data.salary) * 100 : null),
+            valueFormatter: ({ value }) => (value != null ? Number(value).toFixed(2) : ''),
+            aggFunc: 'avg',
+            editable: false,
+            groupRowEditable: true,
+            groupRowValueSetter: bonusRateSetter,
+        },
 
         // Custom aggregation function with a custom groupRowValueSetter
         {
-            headerName: 'sumSq(score)',
+            headerName: 'SumSq',
             field: 'score',
             aggFunc: 'sumOfSquares',
             groupRowValueSetter: sumOfSquaresValueSetter,
@@ -104,7 +140,7 @@ const gridOptions: GridOptions<MetricsRecord> = {
     ],
     defaultColDef: {
         flex: 1,
-        minWidth: 150,
+        minWidth: 120,
         sortable: true,
         filter: true,
         resizable: true,
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/data.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/data.ts
index 09e84d17e2c..989912f9acd 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/data.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/data.ts
@@ -1,54 +1,54 @@
-export interface FileRecord {
+export interface BudgetRecord {
     name: string;
-    size: number;
-    children?: FileRecord[];
+    budget: number;
+    children?: BudgetRecord[];
 }
 
-export function getData(): FileRecord[] {
+export function getData(): BudgetRecord[] {
     return [
         {
-            name: 'Documents',
-            size: 0,
+            name: 'Engineering',
+            budget: 0,
             children: [
                 {
-                    name: 'Reports',
-                    size: 0,
+                    name: 'Frontend',
+                    budget: 0,
                     children: [
-                        { name: 'Q1 Report.pdf', size: 120 },
-                        { name: 'Q2 Report.pdf', size: 150 },
-                        { name: 'Q3 Report.pdf', size: 90 },
+                        { name: 'Alice', budget: 85000 },
+                        { name: 'Bob', budget: 92000 },
+                        { name: 'Charlie', budget: 78000 },
                     ],
                 },
                 {
-                    name: 'Invoices',
-                    size: 0,
+                    name: 'Backend',
+                    budget: 0,
                     children: [
-                        { name: 'Invoice 001.pdf', size: 45 },
-                        { name: 'Invoice 002.pdf', size: 60 },
+                        { name: 'Diana', budget: 95000 },
+                        { name: 'Evan', budget: 88000 },
                     ],
                 },
-                { name: 'README.txt', size: 10 },
+                { name: 'Dave (DevOps)', budget: 90000 },
             ],
         },
         {
-            name: 'Media',
-            size: 0,
+            name: 'Marketing',
+            budget: 0,
             children: [
                 {
-                    name: 'Photos',
-                    size: 0,
+                    name: 'Digital',
+                    budget: 0,
                     children: [
-                        { name: 'Vacation.jpg', size: 250 },
-                        { name: 'Family.jpg', size: 180 },
-                        { name: 'Sunset.jpg', size: 320 },
+                        { name: 'Fiona', budget: 72000 },
+                        { name: 'George', budget: 68000 },
+                        { name: 'Hannah', budget: 75000 },
                     ],
                 },
                 {
-                    name: 'Videos',
-                    size: 0,
+                    name: 'Brand',
+                    budget: 0,
                     children: [
-                        { name: 'Tutorial.mp4', size: 500 },
-                        { name: 'Presentation.mp4', size: 350 },
+                        { name: 'Ian', budget: 80000 },
+                        { name: 'Jane', budget: 76000 },
                     ],
                 },
             ],
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/main.ts
index a7b95301aaf..74297091fde 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/_examples/tree-data-editable/main.ts
@@ -8,7 +8,7 @@ import {
 } from 'ag-grid-community';
 import { RowGroupingEditModule, TreeDataModule } from 'ag-grid-enterprise';
 
-import type { FileRecord } from './data';
+import type { BudgetRecord } from './data';
 import { getData } from './data';
 
 ModuleRegistry.registerModules([
@@ -19,18 +19,18 @@ ModuleRegistry.registerModules([
     ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
 ]);
 
-let gridApi: GridApi<FileRecord>;
+let gridApi: GridApi<BudgetRecord>;
 
-const gridOptions: GridOptions<FileRecord> = {
+const gridOptions: GridOptions<BudgetRecord> = {
     columnDefs: [
         {
-            headerName: 'Size (KB)',
-            field: 'size',
+            headerName: 'Budget',
+            field: 'budget',
             aggFunc: 'sum',
             editable: true,
 
-            // Enable editing on group (folder) rows.
-            // The built-in distribution divides the new total equally among
+            // Enable editing on group (department/team) rows.
+            // The built-in distribution divides the new budget equally among
             // children, cascading through the full tree hierarchy.
             groupRowEditable: true,
             groupRowValueSetter: { precision: 0 },
@@ -40,14 +40,15 @@ const gridOptions: GridOptions<FileRecord> = {
         flex: 1,
     },
     autoGroupColumnDef: {
-        headerName: 'Name',
+        headerName: 'Department / Team / Employee',
         field: 'name',
-        minWidth: 250,
+        minWidth: 280,
         cellRendererParams: { suppressCount: true },
     },
     treeData: true,
     treeDataChildrenField: 'children',
     groupDefaultExpanded: -1,
+    getRowId: ({ data }) => data.name,
     rowData: getData(),
 };
 
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc
index 7648a2e9e8d..5024b9786d8 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-edit/index.mdoc
@@ -7,12 +7,14 @@ The grid supports editing grouped data when using the [Client-Side Row Model](./
 This page covers making group row cells editable, distributing edited values to descendant rows,
 and refreshing the grouping hierarchy after edits.
 
-{% note %}
-`groupRowEditable` and `groupRowValueSetter` require the `RowGroupingEditModule` to be registered.
-{% /note %}
-
 ## Editing Group Row Cells
 
+Double-click any group row's `Sales` cell to edit it. The new value is distributed equally among
+the group's children, rounding to integers. When a child is itself a group, the cascade recurses
+through the full hierarchy.
+
+{% gridExampleRunner title="Built-in Sum Distribution" name="distribute-group-value-sum" exampleHeight=540 /%}
+
 Set `groupRowEditable` on any column that should accept edits on group rows.
 This property accepts either a `boolean` or a callback; the callback is only invoked for nodes
 where `rowNode.group === true`, while leaf rows continue to honour `editable`.
@@ -20,26 +22,17 @@ where `rowNode.group === true`, while leaf rows continue to honour `editable`.
 When a column defines both `groupRowEditable` and `editable`, the grid evaluates only the property
 that matches the current node type, enabling separate rules for group rows and leaf rows.
 
-`groupRowValueSetter` controls how a group row value edit is distributed to descendant rows.
-It can be set to `true` for built-in distribution with defaults, an options object for customised distribution,
-a custom callback function, or `false` to explicitly disable distribution.
-When `groupRowEditable` is defined and `groupRowValueSetter` is not set, the built-in distribution
-is used automatically.
-
-Most grouped rows (and filler nodes in tree data) do not own `rowNode.data`, so the regular column
-`valueSetter` does not run for them. A `groupRowValueSetter` should be provided whenever edits on
-group rows need to persist or cascade to descendant rows.
+When `groupRowEditable` is defined, the grid automatically distributes edited values to descendant
+rows using the built-in distribution. To customise this behaviour, set `groupRowValueSetter` to an
+options object, a custom callback function, or `false` to disable distribution.
 
 {% apiDocumentation source="column-properties/properties.json" section="editing" names=["groupRowEditable", "groupRowValueSetter"] /%}
 
 ## Built-in Value Distribution
 
-The simplest way to make group row totals editable is to set `groupRowEditable: true`.
-When `groupRowEditable` is defined, the built-in distribution is used automatically unless
-`groupRowValueSetter: false` is set to disable it. You can also set `groupRowValueSetter: true`
-explicitly, or pass an options object to customise the distribution strategy.
 The built-in distribution cascades the edited value to descendant rows, adjusting each child
-so the aggregate matches the new total.
+so the aggregate matches the new total. Pass an options object to `groupRowValueSetter` to
+customise the strategy or precision.
 
 ```{% frameworkTransform=true %}
 const gridOptions = {
@@ -50,40 +43,73 @@ const gridOptions = {
             editable: true,
             groupRowEditable: true,
             // Built-in distribution: divides the sum equally among children.
-            // precision: 0 rounds to integers and spreads the remainder.
+            // precision: 0 rounds child values to integers and spreads the remainder.
             groupRowValueSetter: { precision: 0 },
         },
     ],
 }
 ```
 
-{% gridExampleRunner title="Built-in Sum Distribution" name="distribute-group-value-sum" exampleHeight=540 /%}
-
-In this example, double-clicking on any group row's `Sales` cell opens the editor. The built-in
-distribution divides the new value equally among the group's children, rounding to whole numbers.
-When a child is itself a group, the cascade recurses through the full hierarchy.
-
 ### Distribution Strategies
 
 The `distribution` option controls how the edited value is spread across children. When omitted,
 the strategy is chosen automatically based on the column's `aggFunc`:
 
-| Strategy       | Behaviour                                                                                                                | Default for                          |
-| -------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ |
-| `'uniform'`    | Divides the new value equally among all children.                                                                        | `sum`                                |
-| `'overwrite'`  | Writes the new value directly to every child.                                                                            | `avg`, `count`, and custom functions |
-| `'percentage'` | Scales each child proportionally, preserving relative weights. Falls back to `'uniform'` when the current total is zero. | —                                    |
-| `'increment'`  | Distributes only the difference (`newValue − oldValue`) among children, adding it to current values.                     | —                                    |
-| `'none'`       | Suppresses distribution entirely. No child values are modified. Equivalent to `false` or `null`.                         | —                                    |
+| Strategy       | Behaviour                                                                                                                | Default for       |
+| -------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------- |
+| `'uniform'`    | Divides the new value equally among all children.                                                                        | `sum`             |
+| `'overwrite'`  | Writes the new value directly to every child.                                                                            | `avg`, no aggFunc |
+| `'percentage'` | Scales each child proportionally, preserving relative weights. Falls back to `'uniform'` when the current total is zero. | —                 |
+| `'increment'`  | Distributes only the difference (`newValue − oldValue`) among children, adding it to current values.                     | —                 |
+
+Editing is **disabled by default** for `count`, `min`, `max`, `first`, `last`, and
+**custom aggregation functions** because there is no single correct way to split an edited value
+back into children for these functions. To enable editing, you can:
+
+-   Set `distribution` to `'overwrite'` to write the new value directly to every child.
+-   Provide a per-aggFunc record entry with `'overwrite'`, `true`, or a custom callback function
+    that implements application-specific logic (see [Per-Aggregation Strategies](#per-aggregation-strategies)).
+-   Assign a custom [callback function](#custom-distribution-with-a-callback) to `groupRowValueSetter`
+    for full control over the distribution.
+
+```{% frameworkTransform=true %}
+const gridOptions = {
+    columnDefs: [
+        {
+            field: 'price',
+            aggFunc: 'min',
+            editable: true,
+            groupRowEditable: true,
+            // 'overwrite' writes the edited value to every child
+            groupRowValueSetter: { distribution: 'overwrite' },
+        },
+        {
+            field: 'quantity',
+            aggFunc: 'count',
+            editable: true,
+            groupRowEditable: true,
+            // Per-aggFunc record: enable only count with overwrite
+            groupRowValueSetter: { distribution: { count: 'overwrite' } },
+        },
+    ],
+}
+```
+
+Setting `distribution` to `true` uses built-in defaults for `sum`, `avg`, no-aggFunc columns,
+and custom aggregation functions. The `count`/`min`/`max`/`first`/`last` functions remain disabled
+with `true` — use `'overwrite'`, a per-aggFunc record entry, or a custom callback to enable them.
+
+Setting `distribution` to `false` or `null` suppresses distribution and makes the cell not editable,
+even when `groupRowEditable` is `true`.
 
-The `first` and `last` aggregation functions always write to the first or last child respectively,
-regardless of any `distribution` setting. The `min` and `max` aggregation functions always write
-to the single child that currently holds the minimum or maximum value.
+For `avg` aggregation, the distributor adjusts child values so the average equals the edited value.
 
 {% note %}
-For `avg` aggregation, the distributor internally adjusts the target so the average equals the edited value.
-With `'uniform'`, each child is set to `newValue` directly (since the average of identical values equals that value).
-With `'increment'`, the full delta is added to every child (since shifting all children by the same amount shifts the average by that amount).
+The `precision` option rounds the values written to **child rows**, not the group row's displayed value.
+The group value is re-computed by the column's `aggFunc` from the rounded children. For `sum` this is always exact.
+For `avg` the re-aggregated value may differ — for example, distributing `precision: 0` across 3 children
+produces integers, but their average (`10 / 3 ≈ 3.33`) is not an integer. Use a
+[Value Formatter](./value-formatters/) to control how the group row displays the re-aggregated value.
 {% /note %}
 
 ### Distribution Options
@@ -102,8 +128,8 @@ const gridOptions = {
     defaultColDef: {
         editable: true,
         groupRowEditable: true,
-        // Default: round to integers for all columns
-        groupRowValueSetter: { precision: 0 },
+        // Default: round child values to integers, disable count
+        groupRowValueSetter: { precision: 0, distribution: { count: false } },
     },
     columnDefs: [
         {
@@ -113,6 +139,12 @@ const gridOptions = {
             // precision: 0 is inherited from defaultColDef.
             groupRowValueSetter: { distribution: 'percentage' },
         },
+        {
+            field: 'quantity',
+            aggFunc: 'count',
+            // Override defaultColDef's false with true to enable editing for count.
+            groupRowValueSetter: { distribution: { count: true } },
+        },
         {
             field: 'profit',
             aggFunc: 'sum',
@@ -127,24 +159,32 @@ Deep merging only applies when both the `defaultColDef` and the column define `g
 as plain objects. If the column defines a callback function, it replaces the default entirely.
 {% /note %}
 
-### All Aggregation Functions
+### Multiple Aggregation Functions
 
-The built-in distribution handles every standard aggregation function. The following example
-demonstrates a mix of built-in strategies and a custom aggregation function:
+The built-in distribution handles standard aggregation functions. The following example
+demonstrates a mix of built-in strategies, non-aggregated columns, and custom callbacks.
+Double-click any group row cell to edit it:
 
--   **Built-in columns** (`sum`, `avg`, `min`, `max`, `count`, `first`, `last`) use the automatic
-    defaults via `groupRowEditable: true`. The `Salary (sum)` column overrides with
-    `precision: 0` to show per-column configuration.
--   **Custom column** (`sumOfSquares`) uses a custom `groupRowValueSetter` callback that distributes
-    by computing `√(newValue / count)` per child — the correct inverse of sum-of-squares.
+-   **Salary** (`sum`): divides the new total equally among children, rounding to integers
+    (`precision: 0`). For example, editing a group total of 300 across 3 children produces 100 each.
+-   **Bonus** (`avg`): overwrites every child with the edited value (the default for `avg`).
+    For example, editing the group average to 15 sets all children's bonus to 15.
+-   **Projects** (no `aggFunc`): overwrites every child with the edited value (the default when
+    there is no aggregation). The group cell is blank but still editable.
+-   **Rate %** (custom callback): reads each leaf child's salary and computes their bonus as
+    `salary × rate / 100`. For example, entering 20 sets each child's bonus to 20% of their
+    individual salary. Uses `allLeafChildren` to reach all descendant data rows directly.
+-   **SumSq** (custom `sumOfSquares` aggFunc): uses a custom callback that computes
+    `√(newValue / count)` per child — the correct inverse of sum-of-squares.
 
-{% gridExampleRunner title="All Aggregation Functions" name="distribute-group-value-all-agg" exampleHeight=540 /%}
+{% gridExampleRunner title="Multiple Aggregation Functions" name="distribute-group-value-all-agg" exampleHeight=540 /%}
 
 ### Per-Aggregation Strategies
 
 When different columns use different aggregation functions but share a `groupRowValueSetter`
 (e.g. via `defaultColDef`), the `distribution` property can be set to a record mapping each
-aggregation function name to its own strategy:
+aggregation function name to a strategy string, an options object, a custom callback function,
+`true` to enable with built-in defaults, or `false`/`null` to suppress distribution for that aggFunc:
 
 ```{% frameworkTransform=true %}
 const myCustomAgg = (params) => {
@@ -168,6 +208,32 @@ const gridOptions = {
     aggFuncs: {
         myCustomAgg,
     },
+    defaultColDef: {
+        editable: true,
+        groupRowEditable: true,
+        groupRowValueSetter: {
+            precision: 0,
+            distribution: {
+                sum: 'percentage',                          // strategy string
+                avg: { distribution: 'increment' },         // options object
+                myCustomAgg: (params) => {                  // custom callback function
+                    for (const child of params.aggregatedChildren) {
+                        child.setDataValue(params.column, params.newValue, 'data');
+                    }
+                    return true;
+                },
+            },
+        },
+    },
+}
+```
+
+Custom aggregation functions are disabled by default. To enable editing, add an explicit entry in
+the `distribution` record — either a strategy string, an options object, or a custom callback
+function that implements the inverse of the aggregation logic:
+
+```{% frameworkTransform=true %}
+const gridOptions = {
     defaultColDef: {
         editable: true,
         groupRowEditable: true,
@@ -175,9 +241,10 @@ const gridOptions = {
             precision: 0,
             distribution: {
                 sum: 'percentage',
-                avg: { distribution: 'increment' },
+                avg: 'increment',
+                count: 'overwrite',
+                // Custom aggFunc: provide a callback that reverses the aggregation
                 myCustomAgg: (params) => {
-                    // Full custom logic for this aggregation function
                     for (const child of params.aggregatedChildren) {
                         child.setDataValue(params.column, params.newValue, 'data');
                     }
@@ -189,26 +256,53 @@ const gridOptions = {
 }
 ```
 
-Aggregation functions not present in the record fall through to the `default` callback,
-then to overwriting all children (or the aggFunc's own strategy for `first`/`last`/`min`/`max`).
+Aggregation functions not listed in the record fall through to the `default` fallback, then to
+the built-in defaults (see the strategy table above).
+
+### Default Fallback
+
+When multiple custom aggregation functions share the same distribution logic, the `default`
+property avoids repeating entries for each one. It applies to any aggregation function not
+explicitly listed in the `distribution` record — including custom aggregation functions (which are
+disabled without it) and columns without an `aggFunc` (which already default to `'overwrite'` via
+built-in defaults). Non-distributable functions (`count`, `min`, `max`, `first`, `last`) are
+**not** affected by `default` and must always be listed explicitly.
+
+```{% frameworkTransform=true %}
+const gridOptions = {
+    defaultColDef: {
+        editable: true,
+        groupRowEditable: true,
+        groupRowValueSetter: {
+            precision: 0,
+            distribution: {
+                sum: 'percentage',
+                count: 'overwrite',
+            },
+            // Fallback for unlisted custom aggFuncs and no-aggFunc columns.
+            default: 'overwrite',
+        },
+    },
+}
+```
 
 ## Custom Distribution with a Callback
 
 For full control over how group-level edits cascade, assign a function to `groupRowValueSetter`.
-The callback receives a `GroupRowValueSetterParams` object and should return `true` if any
-child value was changed.
+The callback receives a `GroupRowValueSetterParams` object and should return `true` if any child
+value was changed.
 
-{% gridExampleRunner title="Custom Editable Group Totals" name="group-editable-totals-custom" exampleHeight=620 /%}
+The following example demonstrates a custom function that distributes the edited total equally
+among children, rounding child values to integers (largest remainder method). The function calls
+`setDataValue` on each child, which triggers aggregation to refresh parent totals automatically.
 
-In this example, a custom function distributes the edited total equally among children using
-integer rounding (largest remainder method). The function calls `setDataValue` on each child,
-which triggers aggregation to refresh parent totals automatically.
+{% gridExampleRunner title="Custom Editable Group Totals" name="group-editable-totals-custom" exampleHeight=620 /%}
 
-### GroupRowValueSetterParams Reference
+### GroupRowValueSetterParams
 
 {% interfaceDocumentation interfaceName="GroupRowValueSetterParams" /%}
 
-### The aggregatedChildren Parameter
+### Aggregated Children
 
 The `groupRowValueSetter` callback receives an `aggregatedChildren` array containing the immediate children
 that contribute to the [Aggregation](./aggregation/) for the edited column. This array makes it easy to
@@ -234,20 +328,19 @@ See [Retrieving Aggregated Children](./aggregation/#retrieving-aggregated-childr
 
 Key points for implementing cascading edits:
 
--   Calling `rowNode.setDataValue` on each child pushes updated figures down. This triggers normal
-    aggregation to refresh parent totals. When working with large datasets or frequent edits,
-    consider using [Batch Editing](./cell-editing-batch/) to group multiple `setDataValue` calls
-    into a single transaction for better performance.
+-   Calling `rowNode.setDataValue` on each child pushes updated figures down.
 -   When a child is also a group that defines its own `groupRowValueSetter`, the cascade recurses further.
 -   Parent aggregates refresh automatically because child `data` changes re-run the column `aggFunc`,
     so editing a group row total instantly rebalances the children to reach that value.
+-   Aggregation is batched automatically — all `setDataValue` calls within a single `groupRowValueSetter`
+    invocation are processed before re-aggregation runs.
 
 {% note %}
 The `aggregatedChildren` parameter and `rowNode.getAggregatedChildren()` method are only supported
 with the Client-Side Row Model. For other row models, `aggregatedChildren` is an empty array.
 {% /note %}
 
-### Using distributeGroupValue Directly
+### Calling distributeGroupValue
 
 The built-in distribution function `distributeGroupValue` is exported from `ag-grid-enterprise`
 and can be called directly inside a custom `groupRowValueSetter` callback. This is useful when
@@ -271,7 +364,7 @@ import { distributeGroupValue } from 'ag-grid-enterprise';
 colDef.groupRowValueSetter = distributeGroupValue;
 ```
 
-### Custom Read/Write with getValue and setValue
+### Custom getValue and setValue
 
 The `getValue` and `setValue` callbacks let you override how the built-in distributor reads from
 and writes to child rows. By default, the distributor uses `node.getDataValue(column, 'value')`
@@ -293,11 +386,8 @@ const gridOptions = {
             groupRowEditable: true,
             groupRowValueSetter: {
                 distribution: 'percentage',
-                // Read proportions from a separate 'weight' field
-                getValue: (params) => params.data?.weight ?? 0,
-                // Write distributed values to the standard column
-                setValue: (params) =>
-                    params.node.setDataValue(params.column, Math.max(0, Number(params.value)), 'data'),
+                getValue: (params) => (params.data && params.data.weight) || 0,
+                setValue: (params) => params.node.setDataValue(params.column, params.value, 'data'),
             },
         },
     ],
@@ -316,11 +406,8 @@ distributed uniformly among the children matching that pivot column's keys.
 
 {% gridExampleRunner title="Built-in Pivot Editing" name="pivot-editable-builtin" exampleHeight=500 /%}
 
-For full control over pivot editing, use a custom `groupRowValueSetter` callback:
-
-{% gridExampleRunner title="Custom Editable Pivot Totals" name="pivot-editable-totals-custom" exampleHeight=500 /%}
-
-In this example:
+For full control over pivot editing, use a custom `groupRowValueSetter` callback.
+In the following example:
 
 -   The grid is pivoted by `product` (Electronics, Clothing, Food), grouped by `region` and `country`.
 -   Double-clicking on any pivot column cell opens the editor for the aggregated value.
@@ -328,35 +415,36 @@ In this example:
     contains only the European rows selling Electronics — not all European rows.
 -   The edit is distributed equally among just those matching children, leaving other product categories unchanged.
 
+{% gridExampleRunner title="Custom Editable Pivot Totals" name="pivot-editable-totals-custom" exampleHeight=500 /%}
+
 This behaviour ensures that edits on pivot columns affect only the data that contributes to that specific
 pivot value, making cascading edits work correctly with pivoted data.
 
 ## Tree Data
 
 The `groupRowEditable` and `groupRowValueSetter` features also work with [Tree Data](./tree-data/).
-In tree data mode, parent (folder) nodes act as group rows — editing a parent's aggregated value
+In tree data mode, parent nodes act as group rows — editing a parent's aggregated value
 cascades the change down to its children, just like with row grouping.
 
-{% gridExampleRunner title="Tree Data Editable" name="tree-data-editable" exampleHeight=500 /%}
+The following example shows a company's budget organised as a tree: departments contain teams, and
+teams contain employees. Editing a department's or team's budget distributes the new total equally
+among its members, rounding to integers. The cascade recurses through the full hierarchy automatically.
 
-In this example, editing a folder's `Size` distributes the new total equally among its child files,
-with integer rounding. The cascade recurses through nested folders automatically.
+{% gridExampleRunner title="Tree Data Editable" name="tree-data-editable" exampleHeight=500 /%}
 
 ## Refreshing Groups After Editing
 
 When grouped columns are editable, setting `refreshAfterGroupEdit=true` causes the grid to update row data
-and recalculate the grouping after every committed edit.
-
-{% gridExampleRunner title="Refresh After Group Edit" name="refresh-after-group-edit" exampleHeight=600 /%}
-
-In the example above, changing the `department` or `team` causes the grid to re-evaluate the grouping and move
-the row to the correct group instantly. Double-click on a `department` or `team` cell to edit it.
-
-By default, without `refreshAfterGroupEdit` enabled, the row data updates but the grouping does not
-get updated until the next full refresh.
+and recalculate the grouping after every committed edit. Without this option, the row data updates but the
+grouping does not get updated until the next full refresh.
 
 When enabling `refreshAfterGroupEdit`, also provide `getRowId` so that the grid can track rows by
 stable IDs while rebuilding the grouping hierarchy.
 
+The following example demonstrates this behaviour. Double-click on a `department` or `team` cell to edit
+it — the grid re-evaluates the grouping and moves the row to the correct group instantly.
+
+{% gridExampleRunner title="Refresh After Group Edit" name="refresh-after-group-edit" exampleHeight=600 /%}
+
 See also [Read Only Edit](./value-setters/#read-only-edit) for configuring immutable grouped data or
 connecting the grid with a store.
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/_examples/side-bar-row-group-panel/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/_examples/side-bar-row-group-panel/main.ts
index a3bf1383a39..e073ab0d334 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/_examples/side-bar-row-group-panel/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-group-panel/_examples/side-bar-row-group-panel/main.ts
@@ -16,7 +16,7 @@ const gridOptions: GridOptions<IOlympicData> = {
         { field: 'country', enableRowGroup: true, rowGroup: true, hide: true },
         { field: 'year', enableRowGroup: true, rowGroup: true, hide: true },
         { field: 'athlete', minWidth: 180 },
-        { field: 'total' },
+        { field: 'total', enableValue: true, aggFunc: 'sum' },
     ],
     defaultColDef: {
         flex: 1,
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts
index 20e1e009d24..f177ff4af54 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts
@@ -1,4 +1,4 @@
-import { expect, test } from '@utils/grid/test-utils';
+import { ensureGridReady, expect, test, waitForGridContent, waitForRowAnimations } from '@utils/grid/test-utils';
 
 test.agExample(import.meta, () => {
     test.eachFramework('Example', async ({ page, agIdFor }) => {
@@ -13,20 +13,24 @@ test.agExample(import.meta, () => {
 
         // Expand All — United States should now be expanded
         await page.getByRole('button', { name: 'Expand All' }).click();
+        await waitForRowAnimations(page);
         await expect(agIdFor.autoGroupExpanded(unitedStatesId)).toBeVisible();
 
         // Reset to Defaults — back to isGroupOpenByDefault state
         await page.getByRole('button', { name: 'Reset to Defaults' }).click();
+        await waitForRowAnimations(page);
         await expect(agIdFor.autoGroupExpanded(australiaId)).toBeVisible();
         await expect(agIdFor.autoGroupExpanded(australia2004Id)).toBeVisible();
         await expect(agIdFor.autoGroupContracted(unitedStatesId)).toBeVisible();
 
         // Collapse All — everything collapsed
         await page.getByRole('button', { name: 'Collapse All' }).click();
+        await waitForRowAnimations(page);
         await expect(agIdFor.autoGroupContracted(australiaId)).toBeVisible();
 
         // Reset to Defaults — Australia > 2004 expanded again
         await page.getByRole('button', { name: 'Reset to Defaults' }).click();
+        await waitForRowAnimations(page);
         await expect(agIdFor.autoGroupExpanded(australiaId)).toBeVisible();
         await expect(agIdFor.autoGroupExpanded(australia2004Id)).toBeVisible();
         await expect(agIdFor.autoGroupContracted(unitedStatesId)).toBeVisible();
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grouping/index.mdoc
index 68d0841d014..21ae2dc80b4 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grouping/index.mdoc
@@ -19,7 +19,7 @@ The example above uses the following configuration to group rows by their `count
 const gridOptions = {
     columnDefs: [
         { field: 'country', rowGroup: true },
-        { field: 'sport', rowGroup: true },
+        { field: 'year', rowGroup: true },
         // ...other column definitions
     ],
 }
diff --git a/documentation/ag-grid-docs/src/content/docs/keyboard-navigation/index.mdoc b/documentation/ag-grid-docs/src/content/docs/keyboard-navigation/index.mdoc
index 338ddd5754b..40cda54bd6f 100644
--- a/documentation/ag-grid-docs/src/content/docs/keyboard-navigation/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/keyboard-navigation/index.mdoc
@@ -147,7 +147,7 @@ myInput.addEventListener("keydown", event => {
     gridApi.ensureIndexVisible(0);
 
     // scrolls to the first column
-    const firstCol = api.getAllDisplayedColumns()[0];
+    const firstCol = gridApi.getAllDisplayedColumns()[0];
     gridApi.ensureColumnVisible(firstCol);
 
     // sets focus into the first grid cell
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/example.spec.ts
new file mode 100644
index 00000000000..ed79767b830
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/example.spec.ts
@@ -0,0 +1,11 @@
+import { clickAllButtons, ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils';
+
+test.agExample(import.meta, () => {
+    test.eachFramework('Example', async ({ page }) => {
+        // PLACEHOLDER - MINIMAL TEST TO ENSURE GRID LOADS WITHOUT ERRORS
+        await ensureGridReady(page);
+        await waitForGridContent(page);
+        await clickAllButtons(page);
+        // END PLACEHOLDER
+    });
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/fakeServer.ts b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/fakeServer.ts
new file mode 100644
index 00000000000..2c27d527e0d
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/fakeServer.ts
@@ -0,0 +1,254 @@
+import type { IServerSideDatasource, IServerSideGetRowsRequest } from 'ag-grid-community';
+
+type ServerResponse = {
+    success: boolean;
+    rows: IOlympicData[];
+    lastRow: number;
+    pivotResultFields?: string[];
+};
+
+export function createServerSideDatasource(server: {
+    getData: (request: IServerSideGetRowsRequest) => ServerResponse;
+}): IServerSideDatasource {
+    return {
+        getRows: (params) => {
+            console.log('server request', {
+                rowGroups: params.request.rowGroupCols?.map((col) => col.id) ?? [],
+                groupKeys: params.request.groupKeys ?? [],
+                pivots: params.request.pivotCols?.map((col) => col.id) ?? [],
+                values: params.request.valueCols?.map((col) => `${col.id}:${col.aggFunc ?? 'sum'}`) ?? [],
+                sortModel: params.request.sortModel ?? [],
+            });
+            const response = server.getData(params.request);
+
+            setTimeout(() => {
+                if (response.success) {
+                    params.success({
+                        rowData: response.rows,
+                        rowCount: response.lastRow,
+                        pivotResultFields: response.pivotResultFields,
+                    });
+                } else {
+                    params.fail();
+                }
+            }, Math.random() * 1000);
+        },
+    };
+}
+
+export function createFakeServer(allData: IOlympicData[]) {
+    const matchesPivotKey = (row: IOlympicData, pivotColIds: string[], pivotKey: string[]) => {
+        for (let i = 0; i < pivotColIds.length; i++) {
+            if (String((row as any)[pivotColIds[i]]) !== pivotKey[i]) {
+                return false;
+            }
+        }
+        return true;
+    };
+
+    const createPivotField = (pivotKey: string[], valueColId: string) => `${pivotKey.join('_')}_${valueColId}`;
+
+    const toNumber = (value: unknown): number | null => {
+        if (typeof value === 'number' && Number.isFinite(value)) {
+            return value;
+        }
+        if (typeof value === 'string') {
+            const parsed = Number(value);
+            return Number.isFinite(parsed) ? parsed : null;
+        }
+        return null;
+    };
+
+    const aggregateValues = (rows: IOlympicData[], field: string, aggFunc: string): number | string | null => {
+        const values = rows.map((row) => (row as any)[field]);
+
+        switch (aggFunc) {
+            case 'sum': {
+                return values.reduce((sum, value) => sum + (toNumber(value) ?? 0), 0);
+            }
+            case 'min': {
+                let min: number | null = null;
+                for (const value of values) {
+                    const num = toNumber(value);
+                    if (num == null) {
+                        continue;
+                    }
+                    min = min == null ? num : Math.min(min, num);
+                }
+                return min;
+            }
+            case 'max': {
+                let max: number | null = null;
+                for (const value of values) {
+                    const num = toNumber(value);
+                    if (num == null) {
+                        continue;
+                    }
+                    max = max == null ? num : Math.max(max, num);
+                }
+                return max;
+            }
+            case 'avg': {
+                let sum = 0;
+                let count = 0;
+                for (const value of values) {
+                    const num = toNumber(value);
+                    if (num == null) {
+                        continue;
+                    }
+                    sum += num;
+                    count++;
+                }
+                return count > 0 ? sum / count : null;
+            }
+            case 'count': {
+                return values.length;
+            }
+            case 'first': {
+                return values.length ? (values[0] as any) ?? null : null;
+            }
+            case 'last': {
+                return values.length ? (values[values.length - 1] as any) ?? null : null;
+            }
+            default:
+                return values.reduce((sum, value) => sum + (toNumber(value) ?? 0), 0);
+        }
+    };
+
+    const sortRows = (rows: IOlympicData[], sortModel: IServerSideGetRowsRequest['sortModel']) => {
+        if (!sortModel?.length) {
+            return rows;
+        }
+
+        return [...rows].sort((a: any, b: any) => {
+            for (const sort of sortModel) {
+                const left = a[sort.colId];
+                const right = b[sort.colId];
+
+                if (left === right) {
+                    continue;
+                }
+
+                const compare = left > right ? 1 : -1;
+                return sort.sort === 'asc' ? compare : -compare;
+            }
+            return 0;
+        });
+    };
+
+    const compareKeys = (left: string, right: string): number => {
+        const leftNum = toNumber(left);
+        const rightNum = toNumber(right);
+        if (leftNum != null && rightNum != null) {
+            return leftNum - rightNum;
+        }
+        return left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' });
+    };
+
+    return {
+        getData: (request: IServerSideGetRowsRequest): ServerResponse => {
+            let rows = allData;
+            const { rowGroupCols = [], pivotCols = [], groupKeys = [], valueCols = [], sortModel = [] } = request;
+
+            for (let i = 0; i < groupKeys.length; i++) {
+                const key = groupKeys[i];
+                const rowGroupCol = rowGroupCols[i];
+                if (!rowGroupCol) {
+                    continue;
+                }
+                rows = rows.filter((row) => String((row as any)[rowGroupCol.id]) === String(key));
+            }
+
+            const isGrouping = rowGroupCols.length > groupKeys.length;
+            let resultRows: IOlympicData[];
+            let pivotResultFields: string[] | undefined;
+
+            const hasPivot = pivotCols.length > 0 && valueCols.length > 0;
+            const pivotColIds = pivotCols.map((col) => col.id);
+            const pivotKeys = hasPivot
+                ? Array.from(new Set(rows.map((row) => pivotColIds.map((id) => String((row as any)[id])).join('|'))))
+                      .map((key) => key.split('|'))
+                      .sort((a, b) => {
+                          const len = Math.max(a.length, b.length);
+                          for (let i = 0; i < len; i++) {
+                              const cmp = compareKeys(a[i] ?? '', b[i] ?? '');
+                              if (cmp !== 0) {
+                                  return cmp;
+                              }
+                          }
+                          return 0;
+                      })
+                : [];
+
+            const buildPivotedRow = (targetRows: IOlympicData[], base: any = {}) => {
+                const result: any = { ...base };
+                for (const pivotKey of pivotKeys) {
+                    const matchingRows = targetRows.filter((row) => matchesPivotKey(row, pivotColIds, pivotKey));
+                    for (const valueCol of valueCols) {
+                        const field = createPivotField(pivotKey, valueCol.id);
+                        result[field] = aggregateValues(matchingRows, valueCol.id, valueCol.aggFunc!);
+                    }
+                }
+                return result as IOlympicData;
+            };
+
+            if (isGrouping) {
+                const rowGroupCol = rowGroupCols[groupKeys.length];
+                const groupField = rowGroupCol.id;
+                const grouped = new Map<string, IOlympicData[]>();
+
+                for (const row of rows) {
+                    const key = String((row as any)[groupField]);
+                    const bucket = grouped.get(key);
+                    if (bucket) {
+                        bucket.push(row);
+                    } else {
+                        grouped.set(key, [row]);
+                    }
+                }
+
+                resultRows = Array.from(grouped.entries())
+                    .sort(([leftKey, leftRows], [rightKey, rightRows]) => {
+                        if (groupField === 'country') {
+                            const countDiff = rightRows.length - leftRows.length;
+                            if (countDiff !== 0) {
+                                return countDiff;
+                            }
+                        }
+                        return compareKeys(leftKey, rightKey);
+                    })
+                    .map(([key, groupRows]) => {
+                        if (hasPivot) {
+                            return buildPivotedRow(groupRows, { [groupField]: key, childCount: groupRows.length });
+                        }
+
+                        const groupRow: any = { [groupField]: key, childCount: groupRows.length };
+                        for (const valueCol of valueCols) {
+                            groupRow[valueCol.id] = aggregateValues(groupRows, valueCol.id, valueCol.aggFunc!);
+                        }
+                        return groupRow as IOlympicData;
+                    });
+            } else {
+                resultRows = hasPivot ? [buildPivotedRow(rows)] : [...rows];
+            }
+
+            if (hasPivot) {
+                pivotResultFields = pivotKeys.flatMap((pivotKey) =>
+                    valueCols.map((valueCol) => createPivotField(pivotKey, valueCol.id))
+                );
+            }
+
+            const sortedRows = sortRows(resultRows, sortModel);
+            const start = request.startRow ?? 0;
+            const end = request.endRow ?? sortedRows.length;
+            const requestedRows = sortedRows.slice(start, end);
+
+            return {
+                success: true,
+                rows: requestedRows,
+                lastRow: sortedRows.length,
+                pivotResultFields,
+            };
+        },
+    };
+}
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/index.html b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/index.html
new file mode 100644
index 00000000000..ab3ab34e55a
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/index.html
@@ -0,0 +1,3 @@
+<div class="example-wrapper">
+    <div id="myGrid"></div>
+</div>
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/main.ts b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/main.ts
new file mode 100644
index 00000000000..8b159c30d8b
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/main.ts
@@ -0,0 +1,110 @@
+import type { ColDef, GridApi, GridOptions } from 'ag-grid-community';
+import { ModuleRegistry, ValidationModule, createGrid } from 'ag-grid-community';
+import {
+    ColumnMenuModule,
+    ColumnsToolPanelModule,
+    ContextMenuModule,
+    PivotModule,
+    RowGroupingPanelModule,
+    ServerSideRowModelModule,
+} from 'ag-grid-enterprise';
+
+import { createFakeServer, createServerSideDatasource } from './fakeServer';
+
+ModuleRegistry.registerModules([
+    ColumnsToolPanelModule,
+    ColumnMenuModule,
+    ContextMenuModule,
+    PivotModule,
+    RowGroupingPanelModule,
+    ServerSideRowModelModule,
+    ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
+]);
+
+const columnDefs: ColDef[] = [
+    {
+        field: 'athlete',
+        minWidth: 200,
+        enableRowGroup: true,
+        enablePivot: true,
+    },
+    {
+        field: 'age',
+        enableValue: true,
+    },
+    {
+        field: 'country',
+        minWidth: 200,
+        enableRowGroup: true,
+        enablePivot: true,
+        rowGroupIndex: 1,
+    },
+    {
+        field: 'year',
+        enableRowGroup: true,
+        enablePivot: true,
+    },
+    {
+        field: 'date',
+        minWidth: 180,
+        enableRowGroup: true,
+        enablePivot: true,
+    },
+    {
+        field: 'sport',
+        minWidth: 200,
+        enableRowGroup: true,
+        enablePivot: true,
+        rowGroupIndex: 2,
+    },
+    { field: 'gold', hide: true, enableValue: true },
+    { field: 'silver', hide: true, enableValue: true, aggFunc: 'sum' },
+    { field: 'bronze', hide: true, enableValue: true, aggFunc: 'sum' },
+    { headerName: 'Total', field: 'total', enableValue: true },
+];
+
+let gridApi: GridApi<IOlympicData>;
+
+const gridOptions: GridOptions<IOlympicData> = {
+    columnDefs,
+    defaultColDef: {
+        flex: 1,
+        minWidth: 150,
+    },
+    autoGroupColumnDef: {
+        minWidth: 250,
+    },
+    rowModelType: 'serverSide',
+    rowGroupPanelShow: 'always',
+    pivotPanelShow: 'always',
+    sideBar: {
+        toolPanels: [
+            {
+                id: 'columns',
+                labelDefault: 'Columns',
+                labelKey: 'columns',
+                iconKey: 'columns',
+                toolPanel: 'agColumnsToolPanel',
+                toolPanelParams: {
+                    buttons: ['cancel', 'apply'],
+                },
+            },
+        ],
+        defaultToolPanel: 'columns',
+    },
+    getChildCount: (data: any) => (typeof data?.childCount === 'number' ? data.childCount : undefined),
+};
+
+// setup the grid after the page has finished loading
+document.addEventListener('DOMContentLoaded', function () {
+    const gridDiv = document.querySelector<HTMLElement>('#myGrid')!;
+    gridApi = createGrid(gridDiv, gridOptions);
+
+    fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
+        .then((response) => response.json())
+        .then((data: IOlympicData[]) => {
+            const fakeServer = createFakeServer(data);
+            const datasource = createServerSideDatasource(fakeServer);
+            gridApi!.setGridOption('serverSideDatasource', datasource);
+        });
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/styles.css b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/styles.css
new file mode 100644
index 00000000000..072f3617e55
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/deferred-apply-mode/styles.css
@@ -0,0 +1,10 @@
+.example-wrapper {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+#myGrid {
+    flex: 1 1 0px;
+    width: 100%;
+}
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/hide-open-parents/main.ts b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/hide-open-parents/main.ts
index 055efffec03..e666f3cb1ba 100644
--- a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/hide-open-parents/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/_examples/hide-open-parents/main.ts
@@ -51,7 +51,7 @@ function getServerSideDatasource(server: any): IServerSideDatasource {
                     // inform the grid request failed
                     params.fail();
                 }
-            }, 2500);
+            }, 500);
         },
     };
 }
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/index.mdoc b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/index.mdoc
index c7abd9ebd01..a9a17dfcc75 100644
--- a/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-grouping/index.mdoc
@@ -189,12 +189,6 @@ const rowData = {
         code: 'IRE'
     },
 
-    // year field is complex object
-    year: {
-        name: '2012',
-        shortName: "'12"
-    },
-
     // other fields as normal
     ...
 }
@@ -215,3 +209,41 @@ In the example below, note the following:
 -   The example enables `serverSideOnlyRefreshFilteredGroups`, note that if you apply a filter to `Year` with the value `1900`, no leaf rows exist in any group.
 
 {% gridExampleRunner title="Filtering" name="filtering"  exampleHeight=590 /%}
+
+## Batching Data Requests
+
+You can configure the Columns Tool Panel to stage changes and require an explicit **Apply** action before they are committed. This allows multiple configuration changes to be made and applied in a single update, avoiding unnecessary intermediate recomputations or requests.
+
+Deferred Updates are enabled by including the **Apply** button in `toolPanelParams.buttons`.
+
+Note that in the example below:
+
+-   Changes made in the Columns Tool Panel are staged as pending changes.
+-   **Apply** commits all pending changes in a single operation.
+-   **Cancel** discards all pending changes and restores the last applied state.
+
+{% gridExampleRunner title="Deferred Updates" name="deferred-apply-mode"  exampleHeight=830 /%}
+
+```{% frameworkTransform=true %}
+const gridOptions = {
+    sideBar: {
+        toolPanels: [
+            {
+                id: 'columns',
+                labelDefault: 'Columns',
+                labelKey: 'columns',
+                iconKey: 'columns',
+                toolPanel: 'agColumnsToolPanel',
+                toolPanelParams: {
+                    buttons: ['cancel', 'apply'],
+                },
+            },
+        ],
+        defaultToolPanel: 'columns',
+    },
+};
+```
+
+{% note %}
+Changes made outside the Columns Tool Panel — such as dragging columns into the Row Group or Pivot Panels, using the Column Menu, or calling the Grid / Column API — are applied immediately and clear any pending changes. Column pinning, resizing, and group expansion do not clear pending changes.
+{% /note %}
diff --git a/documentation/ag-grid-docs/src/content/docs/server-side-model-pivoting/index.mdoc b/documentation/ag-grid-docs/src/content/docs/server-side-model-pivoting/index.mdoc
index c3625a2da24..2d26ac32b52 100644
--- a/documentation/ag-grid-docs/src/content/docs/server-side-model-pivoting/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/server-side-model-pivoting/index.mdoc
@@ -208,4 +208,8 @@ The example demonstrates the following:
 
 -   In addition to grouping, aggregation and pivot, the example also demonstrates filtering. The columns **Country** and **Year** have grid-provided filters. The column **Age** has an example-provided custom filter. You can use whatever filter you want, as long as your server knows what to do with it.
 
-{% gridExampleRunner title="Slice And Dice" name="slice-and-dice"  exampleHeight=605 /%}
+{% gridExampleRunner title="Slice And Dice" name="slice-and-dice"  exampleHeight=775 /%}
+
+## Batching Data Requests
+
+You can stage multiple data requests by using the Columns Tool Panel. This allows multiple configuration changes to be applied in a single update, avoiding unnecessary intermediate recomputations or server requests. See [SSRM Row Grouping — Batching Data Requests](./server-side-model-grouping/#batching-data-requests) for a detailed example.
diff --git a/documentation/ag-grid-docs/src/content/docs/side-bar/_examples/animation/main.ts b/documentation/ag-grid-docs/src/content/docs/side-bar/_examples/animation/main.ts
index 40ef38fa5dc..2022a45266f 100644
--- a/documentation/ag-grid-docs/src/content/docs/side-bar/_examples/animation/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/side-bar/_examples/animation/main.ts
@@ -1,12 +1,23 @@
 import type { ColDef, GridApi, GridOptions } from 'ag-grid-community';
-import { ClientSideRowModelModule, ModuleRegistry, ValidationModule, createGrid, themeQuartz } from 'ag-grid-community';
-import { ColumnsToolPanelModule, NewFiltersToolPanelModule, SideBarModule } from 'ag-grid-enterprise';
+import {
+    ClientSideRowModelModule,
+    ModuleRegistry,
+    NumberFilterModule,
+    TextFilterModule,
+    ValidationModule,
+    createGrid,
+    themeQuartz,
+} from 'ag-grid-community';
+import { ColumnsToolPanelModule, NewFiltersToolPanelModule, PivotModule, SideBarModule } from 'ag-grid-enterprise';
 
 ModuleRegistry.registerModules([
     ClientSideRowModelModule,
     ColumnsToolPanelModule,
     NewFiltersToolPanelModule,
+    TextFilterModule,
+    NumberFilterModule,
     SideBarModule,
+    PivotModule,
     ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
 ]);
 
diff --git a/documentation/ag-grid-docs/src/content/docs/theming-parameters/_examples/with-params/main.ts b/documentation/ag-grid-docs/src/content/docs/theming-parameters/_examples/with-params/main.ts
index 7059aa85c6e..f338ea8d8ab 100644
--- a/documentation/ag-grid-docs/src/content/docs/theming-parameters/_examples/with-params/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/theming-parameters/_examples/with-params/main.ts
@@ -24,12 +24,20 @@ const gridOptions: GridOptions<IOlympicData> = {
     theme: myTheme,
     columnDefs,
     rowData,
+    rowSelection: { mode: 'multiRow' },
     defaultColDef: {
         editable: true,
         flex: 1,
         minWidth: 100,
         filter: true,
     },
+    onFirstDataRendered: (params) => {
+        params.api.forEachNode((node) => {
+            if (node.rowIndex === 2 || node.rowIndex === 3 || node.rowIndex === 4) {
+                node.setSelected(true);
+            }
+        });
+    },
 };
 
 createGrid(document.querySelector<HTMLElement>('#myGrid')!, gridOptions);
diff --git a/documentation/ag-grid-docs/src/content/docs/theming-popups/_examples/styling-menus/main.ts b/documentation/ag-grid-docs/src/content/docs/theming-popups/_examples/styling-menus/main.ts
index 3b1ff8b58b1..be190a61e7d 100644
--- a/documentation/ag-grid-docs/src/content/docs/theming-popups/_examples/styling-menus/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/theming-popups/_examples/styling-menus/main.ts
@@ -1,4 +1,4 @@
-import type { ColDef, GridApi, GridOptions } from 'ag-grid-community';
+import type { ColDef, FirstDataRenderedEvent, GridApi, GridOptions } from 'ag-grid-community';
 import { ModuleRegistry, createGrid, themeQuartz } from 'ag-grid-community';
 import { AllEnterpriseModule } from 'ag-grid-enterprise';
 
@@ -31,6 +31,9 @@ const gridOptions: GridOptions<IOlympicData> = {
     defaultColDef: {
         filter: true,
     },
+    onFirstDataRendered: (event: FirstDataRenderedEvent) => {
+        event.api.showColumnMenu('athlete');
+    },
 };
 
 // setup the grid after the page has finished loading
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/example.spec.ts
new file mode 100644
index 00000000000..ed79767b830
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/example.spec.ts
@@ -0,0 +1,11 @@
+import { clickAllButtons, ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils';
+
+test.agExample(import.meta, () => {
+    test.eachFramework('Example', async ({ page }) => {
+        // PLACEHOLDER - MINIMAL TEST TO ENSURE GRID LOADS WITHOUT ERRORS
+        await ensureGridReady(page);
+        await waitForGridContent(page);
+        await clickAllButtons(page);
+        // END PLACEHOLDER
+    });
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/index.html b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/index.html
new file mode 100644
index 00000000000..ab3ab34e55a
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/index.html
@@ -0,0 +1,3 @@
+<div class="example-wrapper">
+    <div id="myGrid"></div>
+</div>
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/main.ts b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/main.ts
new file mode 100644
index 00000000000..2ce7f039b82
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/main.ts
@@ -0,0 +1,72 @@
+import type { ColDef, GridApi, GridOptions } from 'ag-grid-community';
+import { ClientSideRowModelModule, ModuleRegistry, ValidationModule, createGrid } from 'ag-grid-community';
+import {
+    ColumnMenuModule,
+    ColumnsToolPanelModule,
+    ContextMenuModule,
+    PivotModule,
+    RowGroupingPanelModule,
+} from 'ag-grid-enterprise';
+
+ModuleRegistry.registerModules([
+    ClientSideRowModelModule,
+    ColumnsToolPanelModule,
+    ColumnMenuModule,
+    ContextMenuModule,
+    PivotModule,
+    RowGroupingPanelModule,
+    ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
+]);
+
+const columnDefs: ColDef[] = [
+    { field: 'athlete', minWidth: 200, enableRowGroup: true, enablePivot: true },
+    { field: 'age', enableValue: true },
+    { field: 'country', minWidth: 200, enableRowGroup: true, enablePivot: true, rowGroup: true },
+    { field: 'year', enableRowGroup: true, enablePivot: true },
+    { field: 'date', minWidth: 180, enableRowGroup: true, enablePivot: true },
+    { field: 'sport', minWidth: 200, enableRowGroup: true, enablePivot: true },
+    { field: 'gold', hide: true, enableValue: true },
+    { field: 'silver', hide: true, enableValue: true, aggFunc: 'sum' },
+    { field: 'bronze', hide: true, enableValue: true, aggFunc: 'sum' },
+    { headerName: 'Total', field: 'total', enableValue: true },
+];
+
+let gridApi: GridApi<IOlympicData>;
+
+const gridOptions: GridOptions<IOlympicData> = {
+    columnDefs,
+    defaultColDef: {
+        flex: 1,
+        minWidth: 150,
+    },
+    autoGroupColumnDef: {
+        minWidth: 250,
+    },
+    rowGroupPanelShow: 'always',
+    pivotPanelShow: 'always',
+    sideBar: {
+        toolPanels: [
+            {
+                id: 'columns',
+                labelDefault: 'Columns',
+                labelKey: 'columns',
+                iconKey: 'columns',
+                toolPanel: 'agColumnsToolPanel',
+                toolPanelParams: {
+                    buttons: ['cancel', 'apply'],
+                },
+            },
+        ],
+        defaultToolPanel: 'columns',
+    },
+};
+
+// setup the grid after the page has finished loading
+document.addEventListener('DOMContentLoaded', function () {
+    const gridDiv = document.querySelector<HTMLElement>('#myGrid')!;
+    gridApi = createGrid(gridDiv, gridOptions);
+
+    fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
+        .then((response) => response.json())
+        .then((data: IOlympicData[]) => gridApi!.setGridOption('rowData', data));
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/styles.css b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/styles.css
new file mode 100644
index 00000000000..072f3617e55
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode-csrm/styles.css
@@ -0,0 +1,10 @@
+.example-wrapper {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+#myGrid {
+    flex: 1 1 0px;
+    width: 100%;
+}
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc
index d21e2ec4fa2..2f00c6480ce 100644
--- a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc
@@ -24,44 +24,6 @@ Selecting columns means different things depending on whether the grid is in piv
 -   **Pivot Mode Off**: When pivot mode is off, selecting a column toggles the visibility of the column. A selected column is visible and an unselected column is hidden. With `allowDragFromColumnsToolPanel=true`, you can drag a column from the tool panel onto the grid and it will become visible.
 -   **Pivot Mode On**: When pivot mode is on, selecting a column will trigger the column to be either aggregated, grouped or pivoted depending on what is allowed for that column.
 
-## Deferred Updates
-
-You can configure the Columns Tool Panel to stage changes and require an explicit **Apply** action before they are committed. This allows multiple configuration changes to be made and applied in a single update, avoiding unnecessary intermediate recomputations or requests.
-
-Deferred Updates are enabled by including the **Apply** button in `toolPanelParams.buttons`.
-
-The example below uses the Server-Side Row Model. Note that in the example below:
-
--   Changes made in the Columns Tool Panel are staged as pending changes.
--   **Apply** commits all pending changes in a single operation.
--   **Cancel** discards all pending changes and restores the last applied state.
-
-{% gridExampleRunner title="Deferred Updates" name="deferred-apply-mode"  exampleHeight=830 /%}
-
-```{% frameworkTransform=true %}
-const gridOptions = {
-    sideBar: {
-        toolPanels: [
-            {
-                id: 'columns',
-                labelDefault: 'Columns',
-                labelKey: 'columns',
-                iconKey: 'columns',
-                toolPanel: 'agColumnsToolPanel',
-                toolPanelParams: {
-                    buttons: ['cancel', 'apply'],
-                },
-            },
-        ],
-        defaultToolPanel: 'columns',
-    },
-};
-```
-
-{% note %}
-Changes made outside the Columns Tool Panel — such as dragging columns into the Row Group or Pivot Panels, using the Column Menu, or calling the Grid / Column API — are applied immediately and clear any pending changes. Column pinning, resizing, and group expansion do not clear pending changes.
-{% /note %}
-
 ## Columns Tool Panel Sections
 
 The Columns Tool Panel is split into different sections as described from the top:
@@ -249,6 +211,46 @@ The example below demonstrates these methods in action. Note the following:
 
 {% gridExampleRunner title="Expand / Collapse Column Groups" name="expand-collapse"  exampleHeight=640 /%}
 
+## Deferred Updates
+
+You can configure the Columns Tool Panel to stage changes and require an explicit **Apply** action before they are committed. This allows multiple configuration changes to be made and applied in a single update, avoiding unnecessary intermediate recomputations or requests.
+
+Deferred Updates are enabled by including the **Apply** button in `toolPanelParams.buttons`.
+
+Note that in the example below:
+
+-   Changes made in the Columns Tool Panel are staged as pending changes.
+-   **Apply** commits all pending changes in a single operation.
+-   **Cancel** discards all pending changes and restores the last applied state.
+
+{% gridExampleRunner title="Deferred Updates" name="deferred-apply-mode-csrm"  exampleHeight=830 /%}
+
+```{% frameworkTransform=true %}
+const gridOptions = {
+    sideBar: {
+        toolPanels: [
+            {
+                id: 'columns',
+                labelDefault: 'Columns',
+                labelKey: 'columns',
+                iconKey: 'columns',
+                toolPanel: 'agColumnsToolPanel',
+                toolPanelParams: {
+                    buttons: ['cancel', 'apply'],
+                },
+            },
+        ],
+        defaultToolPanel: 'columns',
+    },
+};
+```
+
+{% note %}
+Changes made outside the Columns Tool Panel — such as dragging columns into the Row Group or Pivot Panels, using the Column Menu, or calling the Grid / Column API — are applied immediately and clear any pending changes. Column pinning, resizing, and group expansion do not clear pending changes.
+{% /note %}
+
+When using the [Server-Side Row Model](./server-side-model/), Deferred Updates can be used to batch multiple configuration changes into a single server request. See [Batching Data Requests](./server-side-model-grouping/#batching-data-requests) for an SSRM-specific example.
+
 ## Custom Column Layout
 
 The order of columns in the Columns Tool Panel is derived from the `columnDefs` supplied in the grid options, and is kept in sync with the grid when columns are moved by default. However custom column layouts can also be defined by invoking the following method on the Columns Tool Panel Instance:
diff --git a/documentation/ag-grid-docs/src/content/docs/undo-redo-edits/index.mdoc b/documentation/ag-grid-docs/src/content/docs/undo-redo-edits/index.mdoc
index ba6ed34a12a..a1513afe226 100644
--- a/documentation/ag-grid-docs/src/content/docs/undo-redo-edits/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/undo-redo-edits/index.mdoc
@@ -38,7 +38,7 @@ The default number of undo / redo steps is `10`. To change this default the `und
 The following keyboard shortcuts are available when undo / redo is enabled:
 
 -   {% kbd "^ Ctrl" /%}+{% kbd "Z" /%} / {% kbd "Command" /%}+{% kbd "Z" /%}: will undo the last cell edit(s).
--   {% kbd "^ Ctrl" /%}+{% kbd "Y" /%} / {% kbd "Command" /%}+{% kbd "Y" /%}: will redo the last undo.
+-   {% kbd "^ Ctrl" /%}+{% kbd "Y" /%} / {% kbd "Command" /%}+{% kbd "⇧ Shift" /%}+{% kbd "Z" /%}: will redo the last undo.
 
 Note that the grid needs focus for these shortcuts to have an effect.
 
@@ -100,8 +100,8 @@ To see undo / redo in action, try the following:
 -   **Cell Editing**: click and edit some cell values.
 -   **Fill Handle**: drag the fill handle to change a range of cells.
 -   **Copy / Paste**: use {% kbd "^ Ctrl" /%}+{% kbd "C" /%} / {% kbd "^ Ctrl" /%}+{% kbd "V" /%} to copy and paste a range of cells.
--   **Undo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Z" /%} to undo the cell edits.
--   **Redo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Y" /%} to redo the undone cell edits.
+-   **Undo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Z" /%} / {% kbd "Command" /%}+{% kbd "Z" /%} to undo the cell edits.
+-   **Redo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Y" /%} / {% kbd "Command" /%}+{% kbd "⇧ Shift" /%}+{% kbd "Z" /%} to redo the undone cell edits.
 -   **Undo API**: use the 'Undo' button to invoke `gridApi.undoCellEditing()`.
 -   **Redo API**: use the 'Redo' button to invoke `gridApi.redoCellEditing()`.
 -   **Undo / Redo Limit**: only 5 actions are allowed as `undoRedoCellEditingLimit=5`.
@@ -183,8 +183,8 @@ The following example demonstrates how to use complex objects with undo / redo.
     -   **Cell Editing**: click and edit some cell values.
     -   **Fill Handle**: drag the fill handle to change a range of cells.
     -   **Copy / Paste**: use {% kbd "^ Ctrl" /%}+{% kbd "C" /%} / {% kbd "^ Ctrl" /%}+{% kbd "V" /%} to copy and paste a range of cells.
-    -   **Undo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Z" /%} to undo the cell edits.
-    -   **Redo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Y" /%} to redo the undone cell edits.
+    -   **Undo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Z" /%} / {% kbd "Command" /%}+{% kbd "Z" /%} to undo the cell edits.
+    -   **Redo Shortcut**: use {% kbd "^ Ctrl" /%}+{% kbd "Y" /%} / {% kbd "Command" /%}+{% kbd "⇧ Shift" /%}+{% kbd "Z" /%} to redo the undone cell edits.
     -   **Undo API**: use the 'Undo' button to invoke `gridApi.undoCellEditing()`.
     -   **Redo API**: use the 'Redo' button to invoke `gridApi.redoCellEditing()`.
     -   **Undo / Redo Limit**: only 5 actions are allowed as `undoRedoCellEditingLimit=5`.
diff --git a/documentation/ag-grid-docs/src/utils/grid/test-utils.ts b/documentation/ag-grid-docs/src/utils/grid/test-utils.ts
index 0d366f1c246..f8386fb4f74 100644
--- a/documentation/ag-grid-docs/src/utils/grid/test-utils.ts
+++ b/documentation/ag-grid-docs/src/utils/grid/test-utils.ts
@@ -460,6 +460,55 @@ export async function dragOverTo(source: Locator, target: Locator, offsetPositio
     await mouse.up();
 }
 
+/**
+ * Waits until row animations have settled — i.e. no "zombie" rows remain in the DOM.
+ *
+ * Row animations leave zombie rows in the DOM for ~400ms. A zombie and its
+ * replacement share the same `row-id`, causing strict-mode locators to throw a
+ * "resolved to N elements" error. Call this helper after any action that triggers
+ * row expansion/collapse before making strict-mode assertions.
+ *
+ * Implementation notes:
+ * - Yields one animation frame first so that any pending DOM mutations from the
+ *   preceding action are applied before polling begins (avoids a false early pass).
+ * - Checks uniqueness within each row container independently. AG Grid renders the
+ *   same logical row in multiple containers (left-pinned, center, right-pinned);
+ *   those cross-container duplicates are permanent and expected, so a global
+ *   uniqueness check would hang on grids with pinned columns.
+ */
+export async function waitForRowAnimations(page: Page) {
+    // Flush any pending microtasks / rAF callbacks so DOM mutations from the
+    // preceding action are visible before we start polling.
+    await page.evaluate(() => new Promise<void>((resolve) => requestAnimationFrame(resolve)));
+
+    await page.waitForFunction(() => {
+        // AG Grid renders rows across several named containers. Zombie rows only
+        // appear as duplicates *within* the same container, not across containers.
+        const centerContainerSelectors = [
+            '.ag-center-cols-container',
+            '.ag-sticky-top-container',
+            '.ag-sticky-bottom-container',
+            '.ag-floating-top-container',
+            '.ag-floating-bottom-container',
+        ].join(', ');
+
+        for (const container of document.querySelectorAll(centerContainerSelectors)) {
+            const seen = new Set<string>();
+            for (const row of container.querySelectorAll('.ag-row')) {
+                const rowId = row.getAttribute('row-id');
+                if (!rowId) {
+                    continue;
+                }
+                if (seen.has(rowId)) {
+                    return false;
+                }
+                seen.add(rowId);
+            }
+        }
+        return true;
+    });
+}
+
 export async function clickAllButtons(page: Page) {
     // Click all visible buttons in the grid example
     // Don't use buttons within the ag-root-wrapper as these are not part of the example
diff --git a/documentation/update-algolia-indices/package.json b/documentation/update-algolia-indices/package.json
index 3492d12def4..c5ae73f3ff1 100644
--- a/documentation/update-algolia-indices/package.json
+++ b/documentation/update-algolia-indices/package.json
@@ -1,6 +1,6 @@
 {
   "name": "update-algolia-indices",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "Update algolia indices",
   "main": "src/index.ts",
   "type": "module",
diff --git a/esbuild.config.cjs b/esbuild.config.cjs
index 6cf4ee5dfc5..90adb1d952e 100644
--- a/esbuild.config.cjs
+++ b/esbuild.config.cjs
@@ -10,21 +10,24 @@ const cssPlugin = {
     name: 'css-plugin',
     setup(build) {
         build.onLoad({ filter: /\.css$/ }, async (args) => {
-            const css = await require('fs').promises.readFile(args.path, 'utf8');
-            const result = await postcss(postcssPlugins).process(css, {
-                from: args.path,
-                to: args.path,
-            });
+            const rawCSS = await fs.readFile(args.path, 'utf8');
+            const isLegacyCSS = !args.path.includes('/src/');
+
+            // Legacy theme CSS is already processed through Sass, only source
+            // CSS (Theming API) needs PostCSS.
+            const outputCSS = isLegacyCSS
+                ? rawCSS
+                : (await postcss(postcssPlugins).process(rawCSS, { from: args.path, to: args.path })).css;
 
             // UMD builds: non-source CSS (legacy themes) gets injected as <style> tags
             const isUmd = /:(umd|umd:watch)$/.test(process.env.NX_TASK_TARGET_TARGET ?? '');
-            if (isUmd && !args.path.includes('/src/')) {
+            if (isUmd && isLegacyCSS) {
                 return {
-                    contents: `(function(){if(typeof document!=="undefined"){var s=document.createElement("style");s.setAttribute("data-ag-scope","legacy");s.textContent=${JSON.stringify(result.css)};document.head.appendChild(s);}})();`,
+                    contents: `(function(){if(typeof document!=="undefined"){var s=document.createElement("style");s.setAttribute("data-ag-scope","legacy");s.textContent=${JSON.stringify(outputCSS)};document.head.appendChild(s);}})();`,
                     loader: 'js',
                 };
             }
-            return { contents: result.css, loader: 'text' };
+            return { contents: outputCSS, loader: 'text' };
         });
     },
 };
diff --git a/external/ag-shared/.gitrepo b/external/ag-shared/.gitrepo
index 132d5534e01..28871c9d6cc 100644
--- a/external/ag-shared/.gitrepo
+++ b/external/ag-shared/.gitrepo
@@ -6,7 +6,7 @@
 [subrepo]
 	remote = https://github.com/ag-grid/ag-shared.git
 	branch = latest
-	commit = 2459687b24688dc840629ad808471e7ad7945a8f
+	commit = 4010a1312164ee54ebb7b9076c0a7955b4d4b47a
 	parent = d0d058059e38acc51bd4484e3861c57d6cada8b7
 	method = rebase
 	cmdver = 0.4.9
diff --git a/external/ag-shared/prompts/.claude-settings.json b/external/ag-shared/prompts/.claude-settings.json
index 688e7c372d9..144742caf37 100644
--- a/external/ag-shared/prompts/.claude-settings.json
+++ b/external/ag-shared/prompts/.claude-settings.json
@@ -7,14 +7,21 @@
         "allow": [
             "Bash(command -v:*)",
             "Bash(find:*)",
-            "Bash(gh pr checkout:*)",
-            "Bash(gh pr checks:*)",
-            "Bash(gh pr diff:*)",
-            "Bash(gh pr list:*)",
-            "Bash(gh pr review:*)",
-            "Bash(gh pr view:*)",
+            "Bash(gh api:*)",
+            "Bash(gh gist:*)",
+            "Bash(gh pr:*)",
+            "Bash(gh run:*)",
+            "Bash(gh workflow:*)",
+            "Bash(git add:*)",
+            "Bash(git branch:*)",
+            "Bash(git checkout:*)",
+            "Bash(git diff:*)",
             "Bash(git fetch:*)",
+            "Bash(git log:*)",
             "Bash(git ls-tree:*)",
+            "Bash(git stash list:*)",
+            "Bash(git stash show:*)",
+            "Bash(git status:*)",
             "Bash(grep:*)",
             "Bash(jq:*)",
             "Bash(ls:*)",
@@ -31,6 +38,8 @@
             "Bash(nx test:*)",
             "Bash(rg:*)",
             "Bash(touch:*)",
+            "Bash(./external/ag-shared/scripts/setup-prompts/setup-prompts.sh)",
+            "Bash(./external/ag-shared/scripts/setup-prompts/verify-rulesync.sh)",
             "Bash(yarn nx build:*)",
             "Bash(yarn nx format:write:*)",
             "Bash(yarn nx:*)",
diff --git a/external/ag-shared/prompts/commands/docs/_release-docs-review-core.md b/external/ag-shared/prompts/commands/docs/_release-docs-review-core.md
index 25bea5c60f5..30260021229 100644
--- a/external/ag-shared/prompts/commands/docs/_release-docs-review-core.md
+++ b/external/ag-shared/prompts/commands/docs/_release-docs-review-core.md
@@ -46,11 +46,11 @@ Perform a comprehensive documentation review for all docs pages that have been m
 
 **READ THIS FIRST - MANDATORY EXECUTION RULES:**
 
-### Rule 1: Use SlashCommand Tool for ALL Reviews
+### Rule 1: Use Skill Tool for ALL Reviews
 
 **MANDATORY:** When executing documentation reviews (Step 8), you MUST:
 
--   Use: `SlashCommand` tool with the docs review command specified in **Product** configuration
+-   Use: `Skill` tool with the docs review command specified in **Product** configuration
 -   DO NOT: Create custom review prompts for sub-agents
 -   DO NOT: Perform reviews manually
 -   DO NOT: Implement alternative review methods
@@ -112,6 +112,23 @@ export PREVIOUS_BRANCH=<previous_branch>
 export CURRENT_BRANCH=<current_branch or HEAD>
 ```
 
+### Step 1b: Pre-flight Permission Check
+
+Before spawning review sub-agents, ensure the output directories exist and file writes are permitted. Sub-agents that cannot write output files waste significant tokens reporting permission failures.
+
+```bash
+# Create output directories (substitute paths from product config)
+mkdir -p external/prompts/technical-review-plans
+mkdir -p reports/docs-review
+
+# Test write permission
+touch external/prompts/technical-review-plans/.permission-test && rm external/prompts/technical-review-plans/.permission-test
+touch reports/docs-review/.permission-test && rm reports/docs-review/.permission-test
+echo "✓ Write permissions verified"
+```
+
+If either `mkdir` or `touch` fails due to permission restrictions, resolve the permission issue before proceeding to Step 8. All review sub-agents require Write access to these directories.
+
 ### Step 2: Identify Modified Documentation Pages
 
 Find all directly modified documentation files using the **Paths** → Docs file pattern from the product configuration:
@@ -161,16 +178,12 @@ for file in $(cat modified-types.txt); do
     sort -u
 done > modified-api-names.txt
 
-# For each modified API, find docs that reference it
-> api-affected-docs.txt
-for api_name in $(cat modified-api-names.txt); do
-  grep -r "$api_name" ${DOCS_PATH}/ --include="*.mdoc" | \
-    sed "s|${DOCS_PATH}/||" | \
-    sed 's|/index\.mdoc:.*||' | \
-    sort -u >> api-affected-docs.txt
-done
-
-sort -u api-affected-docs.txt -o api-affected-docs.txt
+# Find docs that reference any modified API (single pass for performance)
+API_PATTERN=$(paste -sd'|' modified-api-names.txt)
+grep -rlE "$API_PATTERN" ${DOCS_PATH}/ --include="*.mdoc" | \
+  sed "s|${DOCS_PATH}/||" | \
+  sed 's|/index\.mdoc$||' | \
+  sort -u > api-affected-docs.txt
 echo "Found $(wc -l < api-affected-docs.txt) docs pages referencing modified APIs"
 ```
 
@@ -642,14 +655,14 @@ done
 **CRITICAL REQUIREMENTS:**
 
 1. **ONE PAGE PER SUB-AGENT - NO BATCHING**
-2. **MUST USE SlashCommand TOOL - NO CUSTOM IMPLEMENTATIONS**
+2. **MUST USE Skill TOOL - NO CUSTOM IMPLEMENTATIONS**
 
 #### Execution Pattern
 
 For each documentation page in the **filtered task list**, spawn a dedicated sub-agent that:
 
 1. **Reviews exactly ONE page** (no batching allowed)
-2. **Uses the SlashCommand tool** to invoke the docs review command from **Product** configuration
+2. **Uses the Skill tool** to invoke the docs review command from **Product** configuration
 3. **Validates the review outputs** (files specified in **Verification Paths**)
 
 #### Sub-Agent Prompt Template
@@ -661,11 +674,11 @@ You are reviewing the [PAGE_NAME] documentation page for [PRODUCT_NAME] release
 
 STRICT REQUIREMENTS:
 
-1. MANDATORY: Use SlashCommand tool to invoke the docs review command
-   - Execute: SlashCommand with command "/docs-review [DOCS_PATH]/[PAGE_NAME]/index.mdoc"
+1. MANDATORY: Use Skill tool to invoke the docs review command
+   - Execute: Skill with command "/docs-review [DOCS_PATH]/[PAGE_NAME]/index.mdoc"
    - DO NOT create custom review implementations
    - DO NOT perform manual reviews
-   - DO NOT skip the SlashCommand tool
+   - DO NOT skip the Skill tool
 
 2. Single Page Only
    - Review ONLY: [PAGE_NAME]
@@ -679,7 +692,7 @@ STRICT REQUIREMENTS:
 4. Return Summary
    - Report review status (PASSED / ISSUES FOUND / FAILED)
    - List critical issues if any found
-   - Confirm SlashCommand was used
+   - Confirm Skill was used
 
 Execute the review now.
 ```
@@ -708,14 +721,14 @@ PAGES=$(grep "^- \[ \]" ${FILTERED_LIST} | \
 
 After all agents complete, verify:
 
-1. Each agent used SlashCommand tool (check agent outputs)
+1. Each agent used Skill tool (check agent outputs)
 2. Each page has the review files specified in **Verification Paths**
 3. No agent reviewed multiple pages (batching violation)
 
 **If validation fails:**
 
 -   Identify which pages were not properly reviewed
--   Identify which agents didn't use SlashCommand
+-   Identify which agents didn't use Skill
 -   Re-launch failed agents with corrected strict prompts
 
 #### Handling Large Page Counts
@@ -907,7 +920,7 @@ Format:
 
 -   **Always run the filtering step (Step 6)** before creating task lists - it typically saves 20-40% of review effort
 -   **ONE PAGE PER SUB-AGENT** - Never batch multiple pages into a single agent
--   **USE SlashCommand TOOL** - Always invoke the docs review command via SlashCommand, never create custom review implementations
+-   **USE Skill TOOL** - Always invoke the docs review command via Skill, never create custom review implementations
 -   **Validate outputs** - After reviews, confirm plan and report files exist at standard locations
 
 ### Performance Optimisation
@@ -919,7 +932,7 @@ Format:
 
 ### Quality Assurance
 
--   **Verify SlashCommand usage** - Check agent outputs confirm they used SlashCommand tool
+-   **Verify Skill usage** - Check agent outputs confirm they used Skill tool
 -   **Validate standard outputs** - Ensure review plan and report files are in correct locations
 -   **Check for batching violations** - Confirm no agent reviewed multiple pages
 -   **Example validation** - If Step 3 found example changes, verify examples run correctly before reviewing docs
@@ -927,7 +940,7 @@ Format:
 ### Progress Tracking
 
 -   **Provide status updates** - Report completion progress for long-running reviews (e.g., "35/77 pages reviewed")
--   **Identify failures early** - If agents don't use SlashCommand or output goes to wrong location, stop and fix
+-   **Identify failures early** - If agents don't use Skill or output goes to wrong location, stop and fix
 -   **Wave-based execution** - For 50+ pages, review in waves of 15-20 agents each
 
 ## Benefits of Filtering
diff --git a/external/ag-shared/prompts/guides/browser-support.md b/external/ag-shared/prompts/guides/browser-support.md
new file mode 100644
index 00000000000..afb108b5ce2
--- /dev/null
+++ b/external/ag-shared/prompts/guides/browser-support.md
@@ -0,0 +1,30 @@
+---
+targets: ['*']
+description: 'Browser support policy — minimum API/feature baseline for runtime code'
+globs: ['packages/*/src/**/*.ts', 'packages/*/src/**/*.tsx']
+---
+
+# Browser Support Policy
+
+AG products officially support the **two latest major versions** of each browser listed below. All runtime code must work within this baseline.
+
+## Supported Browsers
+
+### Desktop
+
+- Chrome (two latest major versions)
+- Firefox (two latest major versions)
+- Microsoft Edge (two latest major versions)
+- Safari (two latest major versions)
+
+### Mobile
+
+- Safari on iOS / iPad OS (two latest major versions)
+- Chrome on iOS / iPad OS / Android (two latest major versions)
+
+## Implications for Code
+
+-   **ES2023+ built-ins are safe** (e.g. `Array.prototype.toReversed()`, `Array.prototype.toSorted()`, `Array.prototype.findLast()`). All supported browsers have shipped these.
+-   **Do not use APIs that landed only in the very latest browser release.** Check [Can I Use](https://caniuse.com) or MDN compatibility tables when uncertain — the feature must be available in at least the two most recent major versions of every supported browser.
+-   **No polyfills in runtime bundles.** The library ships zero third-party dependencies; if a feature isn't natively available in the support matrix, don't use it.
+-   **CSS features follow the same rule.** Any CSS used in DOM overlays or UI components must be supported across the matrix.
diff --git a/external/ag-shared/prompts/skills/batch-plunkers/SKILL.md b/external/ag-shared/prompts/skills/batch-plunkers/SKILL.md
index b6d3bbb8d9c..be0d8a18c0d 100644
--- a/external/ag-shared/prompts/skills/batch-plunkers/SKILL.md
+++ b/external/ag-shared/prompts/skills/batch-plunkers/SKILL.md
@@ -126,6 +126,56 @@ Present results as a markdown table:
 
 If any failed, suggest re-running with just the failed assignments by providing the exact `/batch-plunkers` invocation or spec table.
 
+**JIRA note:** If the user asks to post results to JIRA (now or later), always use `contentFormat: "adf"` — JIRA's markdown renderer does not reliably produce clickable links. See Step 4 for the required ADF structure.
+
+## STEP 3.5: Clean Up Background Tasks
+
+After collecting all results, mark every background task as completed using `TaskUpdate` (set `status: "completed"`) for each task ID. This prevents stale pending tasks from polluting the task list in subsequent conversations.
+
+## STEP 4 (Optional): Post Results to JIRA
+
+If the input was a JIRA ticket (Mode A), offer to post the results as a comment on the source ticket.
+
+Use `contentFormat: "adf"` with `mcp__atlassian__addCommentToJiraIssue`. JIRA's markdown renderer does not reliably auto-link URLs, so you must use ADF with explicit link marks to guarantee clickable links. Never use markdown format or tables for this comment — ADF bullet lists are the only format that reliably renders clickable URLs in JIRA.
+
+Format as an ADF `bulletList` where each `listItem` contains a `paragraph` with: title text — ACs — then a clickable URL using an ADF `link` mark.
+
+**Full ADF structure:**
+
+```json
+{
+  "version": 1,
+  "type": "doc",
+  "content": [
+    {
+      "type": "paragraph",
+      "content": [
+        {"type": "text", "text": "QA Plunkers for ... (staging CDN):", "marks": [{"type": "strong"}]}
+      ]
+    },
+    {
+      "type": "bulletList",
+      "content": [
+        {
+          "type": "listItem",
+          "content": [
+            {
+              "type": "paragraph",
+              "content": [
+                {"type": "text", "text": "#1 — AC 8.1.1: Title here — "},
+                {"type": "text", "text": "https://plnkr.co/edit/abc123", "marks": [{"type": "link", "attrs": {"href": "https://plnkr.co/edit/abc123"}}]}
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
+```
+
+Each URL **must** be wrapped in an ADF link mark — without it, URLs appear as plain text in JIRA and are not clickable.
+
 ## Notes
 
 - **Sub-agent autonomy**: Each sub-agent handles the full lifecycle — file creation, CSS sourcing, and API upload via `plnkr.sh`. The main thread only orchestrates and collects results.
diff --git a/external/ag-shared/prompts/skills/example/ag-charts/enterprise-features.md b/external/ag-shared/prompts/skills/example/ag-charts/enterprise-features.md
index 8c5b0ba35d2..c778430f218 100644
--- a/external/ag-shared/prompts/skills/example/ag-charts/enterprise-features.md
+++ b/external/ag-shared/prompts/skills/example/ag-charts/enterprise-features.md
@@ -60,3 +60,16 @@ The community bundle will **silently skip** enterprise features — no errors, j
 2. Does the example use any enterprise axis (ordinal-time, angle-*, radius-*)? → Enterprise
 3. Does the example use any enterprise plugin (zoom, navigator, annotations, etc.)? → Enterprise
 4. Otherwise → Community
+
+## Plugin Examples vs Financial Chart Preset
+
+**Enterprise plugins** (`ranges`, `zoom`, `navigator`, `annotations`, `toolbar`, `scrollbar`, etc.) work on **ordinary cartesian charts** created with `AgCharts.create()`. They are _not_ tied to the financial chart preset.
+
+Use `AgCharts.createFinancialChart()` **only** when the example specifically demonstrates the **financial chart preset** itself — i.e. candlestick/OHLC price-volume layout, the `rangeButtons` convenience flag, or the built-in financial toolbar.
+
+| Scenario | Creation method |
+|----------|----------------|
+| Demonstrating `ranges`, `zoom`, `navigator` on a line/bar/area chart | `AgCharts.create()` |
+| Demonstrating the financial price-volume preset, OHLC/candlestick series | `AgCharts.createFinancialChart()` |
+
+> **Common mistake:** The `ranges` plugin is listed under Plugins, not Presets. A "range buttons" feature on a line chart uses `AgCharts.create()` with `ranges: { ... }` in the options — not `createFinancialChart()`.
diff --git a/external/ag-shared/prompts/skills/git-conventions/SKILL.md b/external/ag-shared/prompts/skills/git-conventions/SKILL.md
index f0f214dd427..ceeb2716002 100644
--- a/external/ag-shared/prompts/skills/git-conventions/SKILL.md
+++ b/external/ag-shared/prompts/skills/git-conventions/SKILL.md
@@ -29,7 +29,7 @@ description: >-
 
 ## Pull Requests
 
--   **Base branch:** `latest` (the default branch — not `main` or `master`). Use a `bX.Y.Z` release branch as base only when the work targets that release.
+-   **Base branch:** Detect the base branch from git context — do not assume `latest`. If the current branch was created from a `bX.Y.Z` release branch, the PR must target that release branch. This includes any changes needed during the current release cycle — code fixes, skill/prompt updates, example fixes, doc corrections. Only target `latest` for work that doesn't need to ship with the current release. To determine the base, run `.rulesync/skills/git-conventions/detect-base-branch.sh` — it compares merge-base distances from HEAD to all `origin/bX.Y.Z` release branches (newest version first) and prints `BASE_BRANCH=<branch>`. Fall back to `origin/latest` if no release branch is closer. The user may not mention which base to use, so always detect it from git ancestry.
 -   **Title:** `AG-XXXX <description>` for JIRA-linked; plain description otherwise. Under 70 characters.
 -   **Multiple tickets:** comma-separate keys (e.g. `CRT-1030, CRT-1044 Fix highlight`)
 -   **Body (JIRA-linked):** JIRA ticket link(s). Add context only when the change needs explanation beyond the ticket.
diff --git a/external/ag-shared/prompts/skills/git-conventions/detect-base-branch.sh b/external/ag-shared/prompts/skills/git-conventions/detect-base-branch.sh
new file mode 100755
index 00000000000..6d6ded00eed
--- /dev/null
+++ b/external/ag-shared/prompts/skills/git-conventions/detect-base-branch.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# Detect the correct base branch for a PR by comparing merge-base distances.
+#
+# Iterates all origin/bX.Y.Z release branches (newest version first) and
+# compares the distance from HEAD to each branch's merge-base.  When the
+# distance increases from one branch to the next older one, the previous
+# (newer) branch is the parent.
+#
+# Output:
+#   BASE_BRANCH=<branch>   — the detected base (e.g. b13.2.0 or latest)
+#
+# Usage:
+#   source detect-base-branch.sh   # sets BASE_BRANCH
+#   — or —
+#   eval "$(bash detect-base-branch.sh)"
+
+set -euo pipefail
+
+git fetch origin --quiet
+
+LATEST_MB=$(git merge-base origin/latest HEAD 2>/dev/null) || LATEST_MB=""
+if [ -n "$LATEST_MB" ]; then
+  LATEST_DIST=$(git rev-list --count "$LATEST_MB..HEAD")
+else
+  LATEST_DIST=999999
+fi
+echo "latest $LATEST_DIST"
+
+PREV_DIST=""
+PREV_REF=""
+PARENT=""
+
+# List all release branches sorted by version number, newest first.
+# No date cutoff — active release branches may be older than expected.
+for ref in $(git branch -r --list 'origin/b[0-9]*' --sort=-version:refname \
+    --format='%(refname:short)'); do
+  mb=$(git merge-base "$ref" HEAD 2>/dev/null) || continue
+  count=$(git rev-list --count "$mb..HEAD")
+  echo "$ref $count"
+
+  if [ -n "$PREV_DIST" ] && [ "$count" -gt "$PREV_DIST" ]; then
+    PARENT="$PREV_REF"
+    echo "PARENT: $PARENT (distance $PREV_DIST)"
+    break
+  fi
+
+  PREV_DIST="$count"
+  PREV_REF="$ref"
+done
+
+# Fallback: if we iterated candidates but the loop ended without finding an
+# increase (e.g. only one candidate), check the last candidate against latest.
+if [ -z "$PARENT" ] && [ -n "$PREV_REF" ] && [ "$PREV_DIST" -lt "$LATEST_DIST" ]; then
+  PARENT="$PREV_REF"
+  echo "PARENT: $PARENT (distance $PREV_DIST)"
+fi
+
+if [ -n "$PARENT" ]; then
+  BASE_BRANCH="${PARENT#origin/}"
+else
+  BASE_BRANCH="latest"
+fi
+
+echo "BASE_BRANCH=$BASE_BRANCH"
diff --git a/external/ag-shared/prompts/skills/jira/SKILL.md b/external/ag-shared/prompts/skills/jira/SKILL.md
index 71926d36387..6bd684422c8 100644
--- a/external/ag-shared/prompts/skills/jira/SKILL.md
+++ b/external/ag-shared/prompts/skills/jira/SKILL.md
@@ -3,7 +3,8 @@ targets: ['*']
 name: jira
 description: >-
   Whenever the user asks to create a JIRA ticket, file a bug, log an issue,
-  write up a ticket, estimate a ticket, size effort, analyse a JIRA issue, do
+  write up a ticket, split out a ticket, split off a feedback point, extract
+  from a ticket, estimate a ticket, size effort, analyse a JIRA issue, do
   product analysis, or link tickets — ALWAYS invoke this skill first. Also
   invoke when **planning** ticket creation — e.g., drafting a plan that includes
   a "create JIRA ticket" step, or when in plan mode discussing what ticket to
@@ -55,7 +56,7 @@ Based on user intent, read the corresponding workflow file (in the `workflows/`
 | Intent | Keywords | Workflow |
 |--------|----------|----------|
 | **Plan** | In plan mode, drafting a plan that includes JIRA ticket creation | `workflows/plan.md` |
-| **Create** | "create a JIRA", "file a bug", "write up a ticket", "log this issue" | `workflows/create.md` |
+| **Create** | "create a JIRA", "file a bug", "write up a ticket", "log this issue", "split out", "split off", "extract from ticket" | `workflows/create.md` |
 | **Estimate** | "estimate", "size", "analyse complexity", "how long", "effort" | `workflows/estimate.md` |
 | **Analyse** | "analyse this issue", "product analysis", "UX analysis", "propose solutions" | `workflows/analyze.md` |
 
@@ -99,7 +100,8 @@ Each ticket has exactly **one** track value. Never set multiple track values on
 - **End numbered items with periods.**
 - Bold: `**text**`.
 - Code: backticks.
-- URLs: Paste raw URLs directly (JIRA auto-links them); avoid `[text](url)` markdown links.
+- **contentFormat**: Use `"markdown"` for all JIRA API calls. This accepts standard markdown syntax and converts it to JIRA's native ADF format.
+- **URLs must use explicit markdown link syntax** — bare URLs will NOT become clickable links in JIRA. Always write `[https://example.com](https://example.com)` instead of just `https://example.com`. The URL should be visible as both the link text and the href (never hide it behind display text like `[Plunker](url)`).
 - Empty sections: Just `N/A`.
 - No comments — all info in description.
 - When creating tickets from analysis/research documents, distil to decisions and recommendations only. Do not reproduce full analysis in the description — link to the analysis document in the "Design Documents" section instead.
@@ -112,6 +114,21 @@ Each ticket has exactly **one** track value. Never set multiple track values on
 
 Follow the exact structure from the template. Do not use free-form markdown headers (`##`), tables, or code blocks for top-level structure.
 
+### Reading JIRA Comments
+
+To read comments on a ticket (e.g., for split-out workflows where you need to find a specific feedback point):
+
+```
+mcp__atlassian__getJiraIssue
+  cloudId: "1565837d-d6d1-4228-bcb2-4cb74df700f2"
+  issueIdOrKey: "AG-XXXXX"
+  fields: ["comment"]
+  expand: "renderedFields"
+  responseContentFormat: "markdown"
+```
+
+**Warning:** The output is large (comments contain full ADF bodies). Pipe through Python/jq to extract text content. The `fetchAtlassian` ARI tool does **not** return comments — always use `getJiraIssue` with `fields: ["comment"]`.
+
 ### Troubleshooting
 
 **Discovering required fields:** Use `mcp__atlassian__getJiraIssueTypeMetaWithFields` with `cloudId`, `projectIdOrKey: 'AG'`, `issueTypeId: '10105'` (Task).
diff --git a/external/ag-shared/prompts/skills/jira/templates/bug.md b/external/ag-shared/prompts/skills/jira/templates/bug.md
index 56390a06d57..b3121d1bef1 100644
--- a/external/ag-shared/prompts/skills/jira/templates/bug.md
+++ b/external/ag-shared/prompts/skills/jira/templates/bug.md
@@ -2,7 +2,7 @@
 
 **TC1 - First test case label**
 
-1. Open and preview https://reproduction-url.
+1. Open and preview [https://reproduction-url](https://reproduction-url).
 
 2. First reproduction step.
 
diff --git a/external/ag-shared/prompts/skills/jira/workflows/create.md b/external/ag-shared/prompts/skills/jira/workflows/create.md
index b558bcb228a..177689ff9fe 100644
--- a/external/ag-shared/prompts/skills/jira/workflows/create.md
+++ b/external/ag-shared/prompts/skills/jira/workflows/create.md
@@ -63,6 +63,7 @@ Use the `mcp__atlassian__createJiraIssue` tool. Substitute component, prefix, an
     "issueTypeName": "Bug|Task",
     "summary": "[<Prefix>] Clear, concise title",
     "description": "Formatted description from template",
+    "contentFormat": "markdown",
     "additional_fields": {
         "components": [{ "name": "<from product file>" }],
         "priority": { "name": "Medium" },
@@ -94,7 +95,7 @@ Bug descriptions should be concise: test cases + notes only. Do not add acceptan
 ```
 **TC1 - Tooltip missing at bar boundary**
 
-1. Open and preview https://plnkr.co/edit/abc123.
+1. Open and preview [https://plnkr.co/edit/abc123](https://plnkr.co/edit/abc123).
 
 2. Hover the mouse over the rightmost edge of any bar in the bar chart.
 
@@ -107,6 +108,29 @@ Bug descriptions should be concise: test cases + notes only. Do not add acceptan
 -   Regression: Introduced in v9.3.0 (works correctly in v9.2.1).
 ```
 
+## Step 4: Post-Creation Steps
+
+After the ticket is created, perform these steps as applicable:
+
+**1. Transition to "To Do" (only if a fixVersion was provided)**
+
+If the user specified a fix version, transition the ticket out of Backlog immediately:
+
+```
+mcp__atlassian__transitionJiraIssue
+  cloudId: "1565837d-d6d1-4228-bcb2-4cb74df700f2"
+  issueIdOrKey: "<new ticket key>"
+  transitionId: "141"
+```
+
+If no fixVersion was set, leave the ticket in Backlog.
+
+**2. Create issue links (if applicable)**
+
+If this ticket was split off from another, or blocks/is blocked by another ticket, create the link using `mcp__atlassian__createIssueLink`. Do not put ticket references only in the description text — use a formal link.
+
+For "split from" links, see the "Issue Link Direction" section below for correct inward/outward usage.
+
 ## Completion Checklist
 
 **Cannot mark complete until ALL checked:**
@@ -118,7 +142,24 @@ Bug descriptions should be concise: test cases + notes only. Do not add acceptan
 - [ ] Track field set correctly.
 - [ ] For Bug and Improvement tickets: Affects Version included.
 - [ ] For Bug and Improvement tickets: Bug template used (reproduction steps, actual/expected).
-- [ ] URLs pasted as raw URLs (not markdown links).
+- [ ] URLs use explicit markdown link syntax `[url](url)` — bare URLs are not clickable in JIRA.
+- [ ] If fixVersion was provided: ticket transitioned from Backlog to "To Do".
+- [ ] Issue links created (if split-off or related tickets exist).
+
+## Issue Link Direction
+
+The "Work item split" link type has: **inward** = "split from", **outward** = "split to".
+
+To express "NEW-TICKET split from PARENT-TICKET":
+- `inwardIssue` = **PARENT-TICKET** (the source — "split from" points here)
+- `outwardIssue` = **NEW-TICKET** (the new ticket — "split to" points here)
+
+Think of it as: the **inward** issue is the one being referenced ("split **from** X"), and the **outward** issue is the one the link is being created on.
+
+## Description Formatting
+
+- **Inline code for option names:** Always wrap API option names, property names, and programmatic values in backticks (e.g., `enableRtl`, `skipNullBars: true`, `bar`). This distinguishes code from prose and improves readability.
+- **Series type references:** When referencing series types, use backtick-wrapped type values (e.g., `bar`) rather than informal names like "bar/column".
 
 ## Critical Rules
 
@@ -128,3 +169,5 @@ Bug descriptions should be concise: test cases + notes only. Do not add acceptan
 4. **No comments** — Put all information in the description.
 5. **No rationale in feature requests** — State **what** and acceptance criteria, not **why**.
 6. **Improvement = internally reported bug** — Always use the bug template for Improvement tickets, not the feature/task template.
+7. **Inline code for options** — Always wrap API option/property names in backticks in descriptions.
+8. **URLs must be explicit markdown links** — Bare URLs are NOT clickable in JIRA. Always write `[https://url](https://url)`, never just `https://url`.
diff --git a/external/ag-shared/prompts/skills/pr-create/SKILL.md b/external/ag-shared/prompts/skills/pr-create/SKILL.md
index c375b2fb79b..26f39242195 100644
--- a/external/ag-shared/prompts/skills/pr-create/SKILL.md
+++ b/external/ag-shared/prompts/skills/pr-create/SKILL.md
@@ -93,16 +93,17 @@ Scan the `external/` directory for symlinked directories that resolve to separat
 
 ### STEP 3: Identify Base Branch
 
-Determine the correct base branch for the PR:
+Determine the correct base branch for the PR. Users often won't specify the base — detect it from git ancestry.
 
-1.  Check if the current branch was created from a `bX.Y.Z` release branch:
+1.  If `${ARGUMENTS}` contains `--base <branch>`, use that override and skip detection.
+2.  Otherwise, run the shared detection script:
     ```bash
-    git log --oneline --decorate --all | head -30
-    git merge-base --is-ancestor origin/latest HEAD && echo "descends from latest"
+    bash .rulesync/skills/git-conventions/detect-base-branch.sh
     ```
-2.  **Default base:** `latest` (the main branch).
-3.  **Release base:** If the branch clearly descends from a `bX.Y.Z` branch (and not `latest`), use that release branch as the base.
-4.  If ambiguous, ask the user which base branch to target.
+    This iterates all `origin/bX.Y.Z` release branches (newest version first), compares merge-base distances, and prints `BASE_BRANCH=<branch>`. If a release branch is closer than `origin/latest`, it is selected. If only one candidate exists and it is closer than `latest`, it is still selected.
+3.  **Release base:** If the nearest parent is `origin/bX.Y.Z`, use that release branch as the base (e.g., `b13.2.0`).
+4.  **Default base:** If no release branch is closer than `origin/latest`, use `latest`.
+5.  If ambiguous, ask the user which base branch to target.
 
 Store the result as `BASE_BRANCH`.
 
diff --git a/external/ag-shared/prompts/skills/pr-split/SKILL.md b/external/ag-shared/prompts/skills/pr-split/SKILL.md
index 34e8ebc392d..e709903f540 100644
--- a/external/ag-shared/prompts/skills/pr-split/SKILL.md
+++ b/external/ag-shared/prompts/skills/pr-split/SKILL.md
@@ -40,6 +40,16 @@ If the user provides a command option of `help`:
 
 Verify the working tree is clean, not on main branch, and gh CLI is authenticated before proceeding.
 
+### Identify Base Branch
+
+Determine the correct base branch — do not assume `latest`. The user's branch may have been created from a release branch. Run the shared detection script:
+
+```bash
+bash .rulesync/skills/git-conventions/detect-base-branch.sh
+```
+
+This iterates all `origin/bX.Y.Z` release branches (newest version first), compares merge-base distances, and prints `BASE_BRANCH=<branch>`. If a release branch is closer than `origin/latest`, it is selected. Otherwise `latest` is used. Store as `BASE_BRANCH` and use it everywhere this skill references the base branch.
+
 ### Extract JIRA Ticket or Branch Prefix
 
 Determine the commit message prefix from the branch name:
@@ -64,7 +74,7 @@ Use a sub-agent (type: `Explore`) to deeply analyse the changes:
 
 ### 1.1 Gather Information
 
-- Get the full diff against base branch (`latest`)
+- Get the full diff against `{BASE_BRANCH}`
 - Review all commit messages and their content
 - Understand the scope: files changed, lines added/removed, packages touched
 
@@ -144,14 +154,14 @@ Create a temporary branch that holds all changes as staged files:
 
 1. Record the current branch name
 2. Create a temporary branch from the current HEAD
-3. Soft reset to the merge base with `latest` (converts all commits to staged changes)
+3. Soft reset to the merge base with `{BASE_BRANCH}` (converts all commits to staged changes)
 
 ### 3.2 Create PR Branches
 
 For each PR in the plan:
 
 **First PR:**
-- Branch from `latest`
+- Branch from `{BASE_BRANCH}`
 - Bring in the relevant files from the temporary branch
 - Use `git checkout <temp-branch> -- <files>` for clean file extraction
 - Use `git add -p` for partial file staging when needed
@@ -198,7 +208,7 @@ This phase is essential. Each PR must be polished until reviewer-ready, not just
 ### 4.1 For Each PR Branch (in order)
 
 1. **Rebase onto base**
-   - First PR: rebase onto `latest`
+   - First PR: rebase onto `{BASE_BRANCH}`
    - Subsequent PRs: rebase onto previous PR branch
 
 2. **Run `/pr-review`** (via sub-agent)
@@ -257,7 +267,7 @@ git push -u origin "${branch_name}"
 
 For each branch, create a draft PR using `gh pr create`:
 
-- First PR targets `latest`
+- First PR targets `{BASE_BRANCH}`
 - Subsequent PRs target the previous PR's branch
 
 **PR Description Template:**
@@ -299,14 +309,14 @@ Split `{original_branch}` into {N} stacked PRs.
 
 | # | Branch | PR | Description | Base |
 |---|--------|-----|-------------|------|
-| 1 | {branch-part-1} | #{pr1} | {desc1} | latest |
+| 1 | {branch-part-1} | #{pr1} | {desc1} | {BASE_BRANCH} |
 | 2 | {branch-part-2} | #{pr2} | {desc2} | {branch-part-1} |
 | ... | ... | ... | ... | ... |
 
 ## Dependency Diagram
 
 ```
-latest
+{BASE_BRANCH}
   └── {branch-part-1} (PR #{pr1})
         └── {branch-part-2} (PR #{pr2})
               └── {branch-part-3} (PR #{pr3})
@@ -316,7 +326,7 @@ latest
 
 1. Review PRs in order (1, 2, 3, ...)
 2. Each PR shows only its incremental changes
-3. To see cumulative changes up to PR N, compare `{branch-part-N}` to `latest`
+3. To see cumulative changes up to PR N, compare `{branch-part-N}` to `{BASE_BRANCH}`
 4. Approve and merge in order; later PRs will auto-update their base
 ```
 
diff --git a/external/ag-shared/prompts/skills/reflect/SKILL.md b/external/ag-shared/prompts/skills/reflect/SKILL.md
index 5a6deaf2463..4dfa441db07 100644
--- a/external/ag-shared/prompts/skills/reflect/SKILL.md
+++ b/external/ag-shared/prompts/skills/reflect/SKILL.md
@@ -3,7 +3,6 @@ targets: ['*']
 name: reflect
 description: 'Analyse the current conversation to identify where agentic configuration (rules, skills, sub-agents, commands) caused friction, and recommend specific improvements. Use this skill when the user says "reflect", "what went wrong", "what could be better", "improve the prompts", or after a long or friction-heavy session. Also use when the user asks to review how skills or rules performed during the conversation.'
 invocable: user-only
-context: fork
 ---
 
 # Reflect — Agentic Configuration Improvement
@@ -14,8 +13,8 @@ Analyse the current conversation to find where agentic configuration caused fric
 
 Parse optional flags from the invocation:
 
-- `--focus skills|rules|agents|commands|all` — narrow analysis scope (default: `all`)
-- Free text — describe a specific pain point if the conversation doesn't make it obvious
+-   `--focus skills|rules|agents|commands|all` — narrow analysis scope (default: `all`)
+-   Free text — describe a specific pain point if the conversation doesn't make it obvious
 
 Examples: `/reflect`, `/reflect --focus skills`, `/reflect the jira skill kept loading when I was just editing code`
 
@@ -59,10 +58,18 @@ Large rules loaded that were irrelevant to the task. Skills auto-triggered when
 
 #### Permission and Configuration Issues
 
-Tool calls denied that should have been allowed. MCP servers unavailable when needed. Hooks blocked a legitimate workflow.
+Tool calls denied that should have been allowed. MCP servers unavailable when needed. Hooks blocked a legitimate workflow. Commands that were manually approved repeatedly that could be pre-allowed.
 
 **Likely root cause**: `.claude-settings.json` gaps, MCP config issues, hook logic too strict.
 
+**Permission recommendations**: When recommending new permissions, assess the risk level:
+
+-   **Low risk** (recommend adding): Read-only commands (`gh run view`, `gh workflow list`, `ls`, `cat`), build/test commands already part of the standard workflow (`yarn nx test`, `yarn nx lint`), and commands that were approved multiple times during the conversation.
+-   **Medium risk** (recommend with caveat): Commands that modify local state but are reversible (`git checkout`, `yarn install`), or commands scoped to specific tools/patterns.
+-   **High risk** (flag but don't recommend auto-adding): Commands with broad write access (`rm`, `git push`), wildcard patterns that could match destructive operations, or commands that interact with external services beyond read access.
+
+Only recommend adding permissions that were actually used or denied during the conversation — do not speculatively suggest permissions.
+
 #### Missing Capabilities
 
 The user needed a workflow that no skill or command covers. The agent had to improvise a multi-step process that should be codified. A repeatable task with no existing automation.
@@ -94,13 +101,13 @@ Do not read all config files upfront — only the ones relevant to identified fr
 
 For each friction signal, determine:
 
-| Field | Description |
-|-------|-------------|
-| **Category** | `skill-misfire`, `rule-gap`, `rule-overload`, `agent-focus`, `context-waste`, `permission-gap`, or `missing-capability` |
-| **Severity** | `high` (blocked work or significant rework), `medium` (friction but worked around), `low` (minor annoyance) |
-| **Config file** | Exact path to the responsible file, or "N/A — new file needed" |
-| **Evidence** | Brief description of what happened in the conversation |
-| **Recommendation** | Specific change: text to add/remove/modify, glob to adjust, frontmatter to change, or new file to create |
+| Field              | Description                                                                                                             |
+| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
+| **Category**       | `skill-misfire`, `rule-gap`, `rule-overload`, `agent-focus`, `context-waste`, `permission-gap`, or `missing-capability` |
+| **Severity**       | `high` (blocked work or significant rework), `medium` (friction but worked around), `low` (minor annoyance)             |
+| **Config file**    | Exact path to the responsible file, or "N/A — new file needed"                                                          |
+| **Evidence**       | Brief description of what happened in the conversation                                                                  |
+| **Recommendation** | Specific change: text to add/remove/modify, glob to adjust, frontmatter to change, or new file to create                |
 
 ---
 
@@ -132,6 +139,7 @@ Present findings using this structure:
 ## Medium Severity
 
 ### M1: {Short title}
+
 {Same structure}
 
 ---
@@ -139,16 +147,20 @@ Present findings using this structure:
 ## Low Severity
 
 ### L1: {Short title}
+
 {Same structure}
 
 ---
 
 ## Recommended Changes
 
-| # | File | Change | Severity |
-|---|------|--------|----------|
-| 1 | `.rulesync/rules/foo.md` | Add section on X | High |
-| 2 | `.rulesync/skills/bar/SKILL.md` | Narrow description trigger | Medium |
+| #   | File                            | Change                     | Severity | Risk |
+| --- | ------------------------------- | -------------------------- | -------- | ---- |
+| 1   | `.rulesync/rules/foo.md`        | Add section on X           | High     | —    |
+| 2   | `.rulesync/skills/bar/SKILL.md` | Narrow description trigger | Medium   | —    |
+| 3   | `.claude-settings.json`         | Add `Bash(gh run view:*)`  | Low      | Low  |
+
+_For permission changes, the Risk column indicates how dangerous the permission is to grant (Low/Medium/High). See Phase 1 § Permission and Configuration Issues for risk classification criteria._
 
 ## New Capabilities Needed
 
@@ -178,16 +190,16 @@ For each approved change:
 1. Read the target file
 2. Apply the modification
 3. Follow `.rulesync/` conventions:
-   - Never target `.claude/` directly — it's generated output
-   - Never modify `CLAUDE.md`, `AGENTS.md`, or `root: true` files
-   - Cross-repo reusable content → `external/ag-shared/prompts/`
-   - Product-specific content → `external/prompts/`
-   - Repo-specific content → `.rulesync/` directly
+    - Never target `.claude/` directly — it's generated output
+    - Never modify `CLAUDE.md`, `AGENTS.md`, or `root: true` files
+    - Cross-repo reusable content → `external/ag-shared/prompts/`
+    - Product-specific content → `external/prompts/`
+    - Repo-specific content → `.rulesync/` directly
 4. After all changes, regenerate and verify:
-   ```bash
-   ./external/ag-shared/scripts/setup-prompts/setup-prompts.sh
-   ./external/ag-shared/scripts/setup-prompts/verify-rulesync.sh
-   ```
+    ```bash
+    ./external/ag-shared/scripts/setup-prompts/setup-prompts.sh
+    ./external/ag-shared/scripts/setup-prompts/verify-rulesync.sh
+    ```
 
 ### Creating new skills
 
@@ -201,19 +213,19 @@ If the report identified missing capabilities and the user chose option 3:
 
 ## Anti-patterns
 
-- **Do not invent problems** — only report friction that actually occurred in the conversation or that the user described. No hypothetical issues.
-- **Do not auto-apply changes** — always present the report first and wait for explicit approval.
-- **Do not load all config files upfront** — only read files relevant to identified friction signals.
-- **Do not critique prompt writing style** — focus on functional issues (misfires, gaps, waste), not aesthetics or formatting preferences.
-- **Do not duplicate `/validate-prompts`** — that skill checks structural path hygiene. This skill checks functional effectiveness.
-- **Do not duplicate `/optimise-context`** — that command audits token budget. This skill audits conversation-level friction.
+-   **Do not invent problems** — only report friction that actually occurred in the conversation or that the user described. No hypothetical issues.
+-   **Do not auto-apply changes** — always present the report first and wait for explicit approval.
+-   **Do not load all config files upfront** — only read files relevant to identified friction signals.
+-   **Do not critique prompt writing style** — focus on functional issues (misfires, gaps, waste), not aesthetics or formatting preferences.
+-   **Do not duplicate `/validate-prompts`** — that skill checks structural path hygiene. This skill checks functional effectiveness.
+-   **Do not duplicate `/optimise-context`** — that command audits token budget. This skill audits conversation-level friction.
 
 ## Relationship to Other Skills
 
-| Skill | Boundary |
-|-------|----------|
-| `/remember` | Reflect identifies *what* to improve; Remember persists the learnings |
+| Skill               | Boundary                                                                     |
+| ------------------- | ---------------------------------------------------------------------------- |
+| `/remember`         | Reflect identifies _what_ to improve; Remember persists the learnings        |
 | `/validate-prompts` | Validates structural correctness; Reflect validates functional effectiveness |
-| `/optimise-context` | Optimises token budget; Reflect optimises guidance quality |
-| `/rulesync` | Provides the conventions Reflect follows when applying changes |
-| `/skill-creator` | Drafts new skills that Reflect identifies as missing capabilities |
+| `/optimise-context` | Optimises token budget; Reflect optimises guidance quality                   |
+| `/rulesync`         | Provides the conventions Reflect follows when applying changes               |
+| `/skill-creator`    | Drafts new skills that Reflect identifies as missing capabilities            |
diff --git a/external/ag-shared/prompts/skills/run-gha-locally/SKILL.md b/external/ag-shared/prompts/skills/run-gha-locally/SKILL.md
new file mode 100644
index 00000000000..cdebbb5de21
--- /dev/null
+++ b/external/ag-shared/prompts/skills/run-gha-locally/SKILL.md
@@ -0,0 +1,348 @@
+---
+targets: ['*']
+name: run-gha-locally
+description: >-
+  Run GitHub Actions workflow jobs locally by reading workflow YAML and
+  extracting shell commands. Use when the user asks to "run CI locally",
+  "reproduce a CI failure", "run a workflow job", "test a GitHub Action
+  locally", "what does the CI do", or wants to debug why a CI job failed.
+  Also use when the user references a specific workflow file or job name
+  they want to execute locally.
+invocable: user-only
+---
+
+# Run GitHub Actions Locally
+
+You are an expert at reading GitHub Actions workflow YAML and extracting the shell commands so they can be run locally without Docker or `act`.
+
+Your goal is to parse the actual workflow file, classify each step, resolve expressions, and execute the commands step-by-step.
+
+## Help
+
+If the user provides a command option of `help`:
+
+-   Explain how to use this prompt.
+-   Explain that this skill reads workflow YAML directly and extracts shell commands — no Docker or `act` required.
+-   DO NOT proceed, exit the prompt immediately after these steps.
+
+## 1. IMPORTANT TOOLING REQUIREMENTS - STOP IF THESE ARE NOT MET
+
+-   Git CLI must be available.
+-   The repository must contain `.github/workflows/` with at least one workflow file.
+-   Shell (bash/zsh) must be available for executing extracted commands.
+-   Use `NX_DAEMON=false` for any Nx-based commands to avoid daemon pipe hangs.
+
+## 2. Workflow
+
+### Phase 1: Identify Workflow and Job
+
+1.  **Find all workflows:**
+
+    ```bash
+    ls .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null
+    ```
+
+2.  **Parse user input from `${ARGUMENTS}`:**
+
+    -   If the user specifies a workflow file name or path, locate it directly.
+    -   If the user specifies a job name, find which workflow contains it.
+    -   If the user provides a CI run URL (e.g. `https://github.com/org/repo/actions/runs/12345`), extract the workflow name from the URL or ask the user which job failed.
+    -   If the user provides a failure log, identify the workflow and job from the log context.
+
+3.  **If ambiguous, ask the user:**
+
+    -   List all workflows with their jobs (read each YAML file's `jobs:` keys).
+    -   Ask the user to pick a workflow and job.
+
+4.  **Read the target workflow file** and identify:
+
+    -   The target job's `steps:` array.
+    -   Workflow-level `env:` block (top-level).
+    -   Job-level `env:` block.
+    -   The job's `strategy.matrix` block (if any).
+    -   The job's `needs:` dependencies (if any).
+    -   The job's `defaults.run.working-directory` (if any).
+
+### Phase 2: Read and Classify Steps
+
+For each step in the target job, classify it into one of three categories:
+
+#### EXECUTE — `run:` steps
+
+These contain shell commands to extract directly.
+
+```yaml
+- name: Build project
+  run: yarn nx build my-package
+```
+
+Extract the `run:` value as the command to execute.
+
+#### INLINE — Local composite actions (`uses: ./path`)
+
+These reference a local composite action within the repository. The path starts with `./`.
+
+```yaml
+- name: Setup environment
+  uses: ./tools/actions/setup-env
+  with:
+    node-version: '20'
+```
+
+To inline these:
+
+1.  Read the composite action's `action.yml` (or `action.yaml`) at the referenced path.
+2.  Process each step in `runs.steps[]` using the same classification rules as the parent workflow:
+    -   **`run:` steps** — extract the shell command directly.
+    -   **`uses: ./path` steps** — recursively inline the nested composite action.
+    -   **`uses: third-party` steps** — apply the same SKIP logic as for workflow steps. If the third-party action performs setup required by later `run:` steps (e.g. installs a tool, configures credentials), note this and suggest a local equivalent command rather than silently dropping it.
+3.  Resolve `${{ inputs.X }}` references using the values from the calling step's `with:` block.
+4.  If the composite action has `inputs` with `default` values, use those when the calling step doesn't provide a `with:` value.
+
+#### SKIP — Third-party `uses:` steps
+
+These reference remote actions from the GitHub marketplace or other repositories.
+
+```yaml
+- name: Checkout code
+  uses: actions/checkout@v4
+```
+
+**Pattern**: Any `uses:` value that does NOT start with `./` is a third-party action. These are CI infrastructure (checkout, setup-node, cache, upload-artifact, etc.) that either:
+
+-   Are unnecessary locally (checkout — you already have the code).
+-   Cannot run outside GitHub Actions (upload-artifact, github-script).
+-   Handle environment setup that's already present locally (setup-node — you have node).
+
+**Always note what's being skipped** so the user understands what CI infrastructure is not being reproduced. Format as a comment:
+
+```
+# SKIP: actions/checkout@v4 — already have local checkout
+# SKIP: actions/setup-node@v4 — using local node installation
+```
+
+**Exception**: If a third-party action performs something substantive that the user needs (e.g. a custom build action), note this and suggest a manual equivalent.
+
+### Phase 3: Resolve Expressions and Directives
+
+#### Expression Resolution
+
+Resolve `${{ ... }}` expressions from the actual workflow YAML context:
+
+| Pattern | Resolution |
+|---------|------------|
+| `${{ env.X }}` | Look up `X` in workflow-level and job-level `env:` blocks. Job-level overrides workflow-level. |
+| `${{ github.event.inputs.X }}` | Show the user the `workflow_dispatch.inputs.X` definition (including `default`). Prompt the user for a value. |
+| `${{ matrix.X }}` | Read `strategy.matrix.X` values from the job. Present choices to the user and substitute the chosen value. |
+| `${{ steps.ID.outputs.KEY }}` | Track via `$GITHUB_OUTPUT` simulation (see below). If the step hasn't run yet, warn the user. |
+| `${{ needs.X.outputs.Y }}` | Warn: this is a cross-job dependency. Ask the user to provide the value, or offer to run the prerequisite job first. |
+| `${{ secrets.X }}` | Warn that this requires a secret. Ask the user if they have the value; skip if not. |
+| `${{ github.head_ref \|\| github.ref_name }}` | Resolve to `$(git branch --show-current)`. |
+| `${{ github.event.pull_request.base.ref }}` | Prompt the user; default to the repository's default branch. |
+| `${{ github.sha }}` | Resolve to `$(git rev-parse HEAD)`. |
+| `${{ github.ref_name }}` | Resolve to `$(git branch --show-current)`. |
+| `${{ github.event_name }}` | Default to `workflow_dispatch`. |
+| `${{ strategy.job-total }}` | Derive from the matrix size, or prompt. |
+| `${{ runner.os }}` | `Linux` on Linux, `macOS` on macOS, `Windows` on Windows. Use `uname -s` to determine. |
+| `${{ runner.arch }}` | Use `uname -m` to determine. |
+| `${{ github.workspace }}` | Resolve to `$(git rev-parse --show-toplevel)`. |
+
+For any unrecognised expression, show it to the user and ask them to provide a value.
+
+#### Step-Level Directives
+
+-   **`working-directory:`** — Wrap the command in a subshell: `(cd <dir> && <command>)`. If the job has `defaults.run.working-directory`, apply it to all `run:` steps unless the step overrides it.
+-   **`env:` on individual steps** — Prepend `export KEY=VALUE` lines before the command.
+-   **`if:` conditions** — Translate simple conditions to bash comments or guards:
+    -   `if: success()` or `if: always()` — note as a comment but always run.
+    -   `if: failure()` — note as a comment; ask the user whether to run.
+    -   `if: ${{ env.X == 'value' }}` — translate to `if [ "$X" = "value" ]; then ... fi`.
+    -   Complex conditions — include as a comment for the user to evaluate.
+-   **`continue-on-error: true`** — Append `|| true` to the command.
+-   **`timeout-minutes:`** — Optionally wrap with `timeout <seconds>`.
+-   **`shell:`** — If a step specifies `shell: python` or similar, note this and adjust accordingly.
+
+#### `$GITHUB_OUTPUT` Simulation
+
+Some steps write outputs that later steps consume:
+
+```bash
+# Before the first step, set up:
+export GITHUB_OUTPUT=$(mktemp)
+```
+
+After each step that writes to `$GITHUB_OUTPUT` (look for `echo "key=value" >> $GITHUB_OUTPUT` or similar patterns), parse the outputs. GitHub Actions supports two formats:
+
+**Simple format**: `key=value` (value may itself contain `=` characters)
+
+**Multiline format** (heredoc-style):
+```
+key<<EOF
+line1
+line2
+EOF
+```
+
+```bash
+# After a step that writes outputs — parse both simple and multiline formats:
+__parse_github_output() {
+    local file="$1" step_id="$2"
+    while IFS= read -r line; do
+        if [[ "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$ ]]; then
+            export "STEP_${step_id}_${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
+        elif [[ "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_]*)\<\<(.+)$ ]]; then
+            local key="${BASH_REMATCH[1]}" delim="${BASH_REMATCH[2]}" val=""
+            while IFS= read -r line && [[ "$line" != "$delim" ]]; do
+                val+="${val:+$'\n'}${line}"
+            done
+            export "STEP_${step_id}_${key}=${val}"
+        fi
+    done < "$file"
+    > "$file"  # Clear for next step
+}
+__parse_github_output "$GITHUB_OUTPUT" "${STEP_ID}"
+```
+
+Then substitute `${{ steps.ID.outputs.KEY }}` with the corresponding variable.
+
+Also set up `$GITHUB_ENV` similarly for steps that write environment variables:
+
+```bash
+export GITHUB_ENV=$(mktemp)
+```
+
+After each step, source any variables written to `$GITHUB_ENV`:
+
+```bash
+if [ -s "$GITHUB_ENV" ]; then
+    set -a
+    source "$GITHUB_ENV"
+    set +a
+    > "$GITHUB_ENV"
+fi
+```
+
+### Phase 4: Present and Execute
+
+1.  **Present the extracted commands** as a numbered list with step names as comments:
+
+    ```bash
+    # === Environment Setup ===
+    export NX_DAEMON=false
+    export GITHUB_OUTPUT=$(mktemp)
+    export GITHUB_ENV=$(mktemp)
+    # ... workflow-level and job-level env vars ...
+
+    # SKIP: actions/checkout@v4 — already have local checkout
+
+    # Step 1: Install dependencies
+    yarn install
+
+    # Step 2: Build packages
+    yarn nx build my-package
+
+    # SKIP: actions/upload-artifact@v4 — CI artifact upload
+
+    # Step 3: Run tests
+    yarn nx test my-package
+    ```
+
+2.  **Execute step-by-step:**
+
+    -   Run one command at a time using the Bash tool.
+    -   Show the output to the user.
+    -   After each step, ask whether to continue to the next step, retry, skip, or abort.
+
+3.  **On failure:**
+
+    -   Show the error output clearly.
+    -   Suggest possible causes (missing dependencies, environment differences, etc.).
+    -   Ask whether to retry the step, skip it, or abort.
+
+## 3. Matrix Handling
+
+When a job uses `strategy.matrix`:
+
+```yaml
+strategy:
+  matrix:
+    node-version: [18, 20]
+    os: [ubuntu-latest, macos-latest]
+```
+
+1.  Read the matrix definition from the YAML.
+2.  Present all combinations to the user.
+3.  Ask which combination to run (or suggest the most relevant one based on the failure context).
+4.  Substitute `${{ matrix.X }}` with the chosen values throughout all steps.
+
+For **dynamic matrices** (e.g. `fromJson(needs.X.outputs.Y)`):
+
+-   Warn the user that the matrix is dynamically generated.
+-   Ask the user to provide the matrix values, or offer to run the prerequisite job that generates them.
+
+## 4. Cross-Job Dependencies
+
+When a job has `needs:` and references outputs from other jobs:
+
+```yaml
+jobs:
+  build:
+    outputs:
+      artifact-path: ${{ steps.build.outputs.path }}
+  test:
+    needs: build
+    steps:
+      - run: echo ${{ needs.build.outputs.artifact-path }}
+```
+
+1.  **Identify** all `needs:` dependencies and what outputs are consumed.
+2.  **Explain** to the user that this job depends on outputs from another job.
+3.  **Offer options:**
+    -   Run the prerequisite job's commands first (recursively apply this skill).
+    -   Ask the user to provide the output values manually.
+    -   Skip steps that depend on unavailable outputs.
+
+## 5. Important Guidelines
+
+### Environment Differences
+
+Local execution differs from GitHub Actions in several ways. Note these to the user when relevant:
+
+-   **No `GITHUB_TOKEN`**: API calls requiring authentication will fail unless the user provides a token.
+-   **No artifact storage**: Upload/download artifact steps are skipped.
+-   **No service containers**: Jobs using `services:` (e.g. PostgreSQL) need manual setup.
+-   **No job concurrency controls**: `concurrency:` groups don't apply locally.
+-   **Different OS**: The user's local OS may differ from the runner OS.
+
+### Error Recovery
+
+If a step fails:
+
+1.  Check if it's a missing dependency — suggest installing it.
+2.  Check if it's an environment variable issue — check which expressions weren't resolved.
+3.  Check if it's a path issue — working directory may differ from CI.
+4.  If the failure matches what the user is trying to reproduce, congratulate them — the CI failure has been reproduced locally.
+
+### Performance Tips
+
+-   Use `NX_DAEMON=false` for Nx commands.
+-   Skip unnecessary build steps if the user only wants to reproduce a specific test failure.
+-   Suggest running only the failing step if the earlier steps are known to succeed.
+
+## 6. Command Arguments
+
+**Format:** `${ARGUMENTS}` can be:
+
+-   `<workflow-file> <job-name>` — Run a specific job from a specific workflow.
+-   `<workflow-file>` — List jobs in the workflow and ask user to pick.
+-   `<job-name>` — Search all workflows for this job name.
+-   `<github-actions-url>` — Extract workflow/job from the URL.
+-   (empty) — List all workflows and ask user to pick.
+
+**Examples:**
+
+-   `/run-gha-locally ci.yml lint`
+-   `/run-gha-locally ci.yml`
+-   `/run-gha-locally lint`
+-   `/run-gha-locally https://github.com/org/repo/actions/runs/12345`
+-   `/run-gha-locally` (interactive selection)
diff --git a/external/ag-shared/prompts/skills/sync-ag-shared/SKILL.md b/external/ag-shared/prompts/skills/sync-ag-shared/SKILL.md
index 205a690e1d4..8b94746bba7 100644
--- a/external/ag-shared/prompts/skills/sync-ag-shared/SKILL.md
+++ b/external/ag-shared/prompts/skills/sync-ag-shared/SKILL.md
@@ -94,23 +94,41 @@ If any destination has uncommitted changes, default to stashing all changes and
 
 ## STEP 2: Analyse Source Changes
 
-Use a sub-agent (Task tool, `subagent_type: Explore`) to analyse changes on the source branch:
+### 2a. Determine the Last Sync Point
+
+The correct baseline for detecting unsynced changes is the `parent` commit in `.gitrepo` — this records the host-repo commit at the time of the last `subrepo push` or `pull`. Changes committed to `external/ag-shared/` after this commit (whether on `latest` or a feature branch) have not yet been synced.
 
 ```bash
-# Changes inside ag-shared
-git diff latest...HEAD -- external/ag-shared/
+# Extract the parent commit from .gitrepo
+SYNC_PARENT=$(git config -f external/ag-shared/.gitrepo subrepo.parent)
+```
+
+### 2b. Analyse Changes
+
+Use a sub-agent (Task tool, `subagent_type: Explore`) to analyse changes since the last sync:
+
+```bash
+# Changes inside ag-shared since last sync
+git diff "${SYNC_PARENT}"..HEAD -- external/ag-shared/
+
+# Commit log for ag-shared changes since last sync
+git log --oneline "${SYNC_PARENT}"..HEAD -- external/ag-shared/
 
-# Changes outside ag-shared
-git diff latest...HEAD -- ':!external/ag-shared/'
+# Companion changes outside ag-shared that relate to ag-shared (since last sync)
+# These are source-repo-specific config/linkage changes that destinations will
+# need their own equivalent of (e.g. .rulesync/ symlinks, .claude/settings.json,
+# setup-prompts output).
+git diff "${SYNC_PARENT}"..HEAD -- .rulesync/ .claude/settings.json .claude/settings.local.json
 
-# Commit log
-git log --oneline latest...HEAD
+# Full branch commit log (for context)
+git log --oneline "${SYNC_PARENT}"..HEAD
 ```
 
 The sub-agent should produce:
 
-1.  **Change summary** — what files changed in `external/ag-shared/` and why.
-2.  **Companion change predictions** — based on the ag-shared changes, what companion changes are likely needed in each destination repo. For example:
+1.  **ag-shared change summary** — what files changed in `external/ag-shared/` and why.
+2.  **Source companion changes** — what changed in `.rulesync/`, `.claude/settings.json`, or other ag-shared-related files outside `external/ag-shared/`. These represent configuration/linkage changes the source repo already applied that destinations will need their own equivalent of. Cross-reference with `external/ag-shared/docs/SYNC-LOG.md` entries to identify any migration actions that may have been applied in the source but not yet logged or propagated.
+3.  **Companion change predictions** — based on the ag-shared changes AND the source companion changes, what companion changes are likely needed in each destination repo. For example:
     -   New/renamed skills may need symlink updates in `.rulesync/`.
     -   Changed rule globs may need `.claude/settings.json` updates.
     -   Script changes may need `package.json` or CI updates.
@@ -118,14 +136,14 @@ The sub-agent should produce:
 
 ### No Changes Detected (Force Sync)
 
-If `git diff latest...HEAD` shows no changes (i.e., the branch is at the same commit as `latest` or has no ag-shared changes):
+If `git diff "${SYNC_PARENT}"..HEAD -- external/ag-shared/` shows no changes (i.e., `external/ag-shared/` has not changed since the last subrepo push/pull):
 
-1.  Inform the user that no local changes were found relative to `latest`.
+1.  Inform the user that no ag-shared changes were found since the last sync (show the `SYNC_PARENT` commit hash and date for context).
 2.  Use `AskUserQuestion` to ask whether they want to proceed with a **force sync** — this will `yarn subrepo push ag-shared` to push the current `external/ag-shared/` state to the ag-shared remote, then pull it into all destination repos. This is useful when:
     -   The ag-shared remote is out of sync with the consuming repos.
     -   A previous sync was incomplete or failed partway through.
-    -   Changes were committed directly to `latest` and need propagating.
-3.  If the user confirms, continue to Step 3 with an empty change summary. The plan should note this is a **force sync** with no new changes on the branch.
+    -   Destination repos missed a previous sync.
+3.  If the user confirms, continue to Step 3 with an empty change summary. The plan should note this is a **force sync** with no new changes since last sync.
 4.  If the user declines, **STOP**.
 
 ## STEP 3: Present Plan and Confirm
@@ -139,13 +157,13 @@ Display to the user:
 **Destinations:** <list of destination repos>
 
 ### Changes in ag-shared
-<summary from step 2>
+<ag-shared change summary from step 2>
 
-### Changes outside ag-shared
-<summary from step 2>
+### Source Companion Changes
+<source companion changes from step 2 — .rulesync/, .claude/settings.json, etc.>
 
-### Predicted Companion Changes
-<per-destination predictions from step 2>
+### Predicted Companion Changes per Destination
+<per-destination predictions from step 2, informed by both ag-shared changes and source companion changes>
 
 ### Steps
 1. Push ag-shared from <SOURCE_REPO>
diff --git a/external/ag-shared/scripts/install-for-cloud/install-for-cloud.sh b/external/ag-shared/scripts/install-for-cloud/install-for-cloud.sh
index e482824605f..4a7b36c53fd 100755
--- a/external/ag-shared/scripts/install-for-cloud/install-for-cloud.sh
+++ b/external/ag-shared/scripts/install-for-cloud/install-for-cloud.sh
@@ -225,7 +225,7 @@ install_dependencies() {
         fi
     else
         log_info "Installing/updating dependencies"
-        if ! yarn install --ci; then
+        if ! yarn install --ci --prefer-offline; then
             log_error "Failed to install dependencies"
             return 2
         fi
diff --git a/external/ag-shared/scripts/setup-worktree/setup-worktree.sh b/external/ag-shared/scripts/setup-worktree/setup-worktree.sh
index ae7f56dca75..7eb9d9ad9be 100755
--- a/external/ag-shared/scripts/setup-worktree/setup-worktree.sh
+++ b/external/ag-shared/scripts/setup-worktree/setup-worktree.sh
@@ -92,7 +92,7 @@ main() {
         exec ./external/ag-shared/scripts/install-for-cloud/install-for-cloud.sh
     else
         log_info "Running yarn install"
-        yarn install
+        yarn install --prefer-offline
     fi
 }
 
diff --git a/package.json b/package.json
index 9d83531a869..634e1345c2f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "license": "MIT",
   "scripts": {
     "compressVideo": "tsx external/ag-website-shared/scripts/compress-video",
diff --git a/packages/ag-grid-angular/package.json b/packages/ag-grid-angular/package.json
index e0c21c9e7ed..35c614147ba 100644
--- a/packages/ag-grid-angular/package.json
+++ b/packages/ag-grid-angular/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-angular",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "AG Grid Angular Component",
   "scripts": {
     "clean": "rimraf dist",
@@ -15,7 +15,7 @@
   "module": "./dist/ag-grid-angular/fesm2022/ag-grid-angular.mjs",
   "typings": "./dist/ag-grid-angular/index.d.ts",
   "dependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "@angular/animations": "^18.0.7",
     "@angular/common": "^18.0.7",
     "@angular/compiler": "^18.0.7",
@@ -27,7 +27,7 @@
     "zone.js": "~0.15.1"
   },
   "devDependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "@angular-devkit/build-angular": "^18.0.7",
     "@angular/cli": "^18.0.7",
     "@angular/forms": "^18.0.7",
diff --git a/packages/ag-grid-angular/projects/ag-grid-angular/package.json b/packages/ag-grid-angular/projects/ag-grid-angular/package.json
index 56cb8298bfd..513d4dfa306 100644
--- a/packages/ag-grid-angular/projects/ag-grid-angular/package.json
+++ b/packages/ag-grid-angular/projects/ag-grid-angular/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-angular",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "AG Grid Angular Component",
   "license": "MIT",
   "peerDependencies": {
@@ -8,7 +8,7 @@
     "@angular/core": ">= 18.0.0"
   },
   "dependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "tslib": "^2.3.0"
   },
   "repository": {
diff --git a/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts b/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts
index 0ee8eceea0c..953dc46b70b 100644
--- a/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts
+++ b/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts
@@ -1270,10 +1270,11 @@ export class AgGridAngular<TData = any, TColDef extends ColDef<TData> = ColDef<a
      * @agModule `RowDragModule`
      */
     @Input({ transform: booleanAttribute }) public rowDragManaged: boolean | undefined = undefined;
-    /** When `true`, managed row dragging updates grouped column values so rows can move between groups. When `false`,
-     * managed dragging only reorders rows inside their existing group.
+    /** When `true`, the grid re-evaluates the grouping hierarchy after editing a grouped column value,
+     * moving the row to the correct group instantly. Also enables managed row dragging to update
+     * grouped column values so rows can move between groups.
      * @default false
-     * @agModule `RowDragModule`
+     * @agModule `RowGroupingModule` / `TreeDataModule`
      */
     @Input({ transform: booleanAttribute }) public refreshAfterGroupEdit: boolean | undefined = undefined;
     /** Used if rowDragManaged is enabled and treeData is enabled,
diff --git a/packages/ag-grid-community/package.json b/packages/ag-grid-community/package.json
index 8c6fb1e484d..181b6605e4a 100644
--- a/packages/ag-grid-community/package.json
+++ b/packages/ag-grid-community/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-community",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
   "main": "./dist/package/main.cjs.js",
   "types": "./dist/types/src/main.d.ts",
@@ -119,7 +119,7 @@
   ],
   "homepage": "https://www.ag-grid.com/",
   "dependencies": {
-    "ag-charts-types": "13.1.0-beta.20260320"
+    "ag-charts-types": "13.2.0-beta.20260324"
   },
   "devDependencies": {
     "web-streams-polyfill": "^3.3.2",
diff --git a/packages/ag-grid-community/src/clientSideRowModel/deltaSort.ts b/packages/ag-grid-community/src/clientSideRowModel/deltaSort.ts
index 3fe5db99287..81b188506ff 100644
--- a/packages/ag-grid-community/src/clientSideRowModel/deltaSort.ts
+++ b/packages/ag-grid-community/src/clientSideRowModel/deltaSort.ts
@@ -62,7 +62,7 @@ export const doDeltaSort = (
     const touchedRows: RowNode[] = [];
     for (let i = 0; i < unsortedRowsLen; ++i) {
         const node = unsortedRows[i];
-        if (updates.has(node) || adds.has(node) || !changedPath || changedPath.hasRow(node)) {
+        if (updates.has(node) || adds.has(node) || changedPath?.hasRow(node)) {
             indexByNode.set(node, ~i); // Bitwise NOT for touched (negative)
             touchedRows.push(node);
         } else {
diff --git a/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts b/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts
index acfbe5585b1..c5e8ea5825a 100644
--- a/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts
+++ b/packages/ag-grid-community/src/edit/strategy/strategyUtils.ts
@@ -81,14 +81,11 @@ export function isCellEditable(beans: BeanCollection, editPosition: Required<Edi
 
     const editable = colDef.editable;
 
-    if (rowNode.group) {
-        const groupRowEditable = colDef.groupRowEditable;
-        if (groupRowEditable != null) {
-            if (column.isColumnFunc(rowNode, groupRowEditable)) {
-                return true;
-            }
-            return existingEditing(beans, editPosition);
+    if (rowNode.group && colDef.groupRowEditable != null) {
+        if (beans.rowGroupingEditValueSvc?.isGroupCellEditable(rowNode, column)) {
+            return true;
         }
+        return existingEditing(beans, editPosition);
     }
 
     if (column.isColumnFunc(rowNode, editable)) {
diff --git a/packages/ag-grid-community/src/entities/colDef-groupRowValueSetter.ts b/packages/ag-grid-community/src/entities/colDef-groupRowValueSetter.ts
index da8523b1e6b..120f81106a9 100644
--- a/packages/ag-grid-community/src/entities/colDef-groupRowValueSetter.ts
+++ b/packages/ag-grid-community/src/entities/colDef-groupRowValueSetter.ts
@@ -168,8 +168,9 @@ export interface DistributionSetValueParams<TData = any, TValue = any, TContext
  *   For `sum`, each child receives `delta / childCount` added to its current value.
  *   For `avg`, the full delta is added to every child.
  * - **`'overwrite'`** — Writes `newValue` directly to every child.
- * - **`'none'`** — Suppresses distribution. The edit is accepted but no child values
- *   are modified. Equivalent to `false` or `null`.
+ *
+ * Use `false` or `null` to suppress distribution entirely — no child values are modified
+ * and the cell is treated as not editable (overriding `groupRowEditable`).
  *
  * @example
  * ```ts
@@ -177,12 +178,17 @@ export interface DistributionSetValueParams<TData = any, TValue = any, TContext
  * colDef.groupRowValueSetter = { distribution: 'percentage' };
  * ```
  */
-export type GroupRowValueSetterDistribution = 'uniform' | 'percentage' | 'increment' | 'overwrite' | 'none';
+export type GroupRowValueSetterDistribution = 'uniform' | 'percentage' | 'increment' | 'overwrite';
 
 /**
  * A value in the `distribution` record. Can be:
  * - A {@link GroupRowValueSetterDistribution} strategy string (e.g. `'percentage'`).
- * - `false` or `null` — equivalent to `'none'`, suppresses distribution for this aggFunc.
+ * - `true` — uses the built-in default for the aggFunc. Enables custom aggFuncs with `'overwrite'`.
+ *   Note: `count`/`min`/`max`/`first`/`last` are only enabled via explicit per-aggFunc record entries.
+ *   Useful for overriding `false`/`null` from a parent in deep-merge scenarios.
+ * - `false` or `null` — suppresses distribution for this aggFunc
+ *   and makes the cell not editable when that aggFunc is active.
+ * - `undefined` — inherits from the parent options (falls through to the default for that aggFunc).
  * - A {@link GroupRowValueSetterDistributionOptions} object with strategy and per-aggFunc overrides.
  * - A custom {@link GroupRowValueSetterFunc} callback for full control.
  *
@@ -191,20 +197,24 @@ export type GroupRowValueSetterDistribution = 'uniform' | 'percentage' | 'increm
  * distribution: {
  *     sum: 'percentage',                                // strategy string
  *     avg: { distribution: 'increment', precision: 0 }, // options object
- *     count: false,                                     // suppress
+ *     count: true,                                      // enable with built-in default
+ *     min: false,                                       // suppress
  *     myCustomAgg: (params) => { ... },                 // custom callback
  * }
  * ```
  */
 export type GroupRowValueSetterDistributionEntry<TData = any, TValue = any, TContext = any> =
     | GroupRowValueSetterDistribution
-    | false
-    | null
     | GroupRowValueSetterDistributionOptions
-    | GroupRowValueSetterFunc<TData, TValue, TContext>;
+    | GroupRowValueSetterFunc<TData, TValue, TContext>
+    | boolean
+    | null
+    | undefined;
 
 /**
  * Maps aggregation function names (e.g. `'sum'`, `'avg'`, or a custom name) to distribution entries.
+ * Each entry can be a strategy string, an options object, a custom callback function,
+ * or `false`/`null` to suppress distribution for that aggFunc.
  * Unmatched aggFuncs fall through to {@link GroupRowValueSetterOptions.default | default},
  * then to the built-in defaults.
  *
@@ -214,6 +224,8 @@ export type GroupRowValueSetterDistributionEntry<TData = any, TValue = any, TCon
  *     distribution: {
  *         sum: 'percentage',
  *         avg: { distribution: 'increment' },
+ *         myCustomAgg: (params) => { ... },
+ *         count: false,
  *     },
  * };
  * ```
@@ -240,16 +252,21 @@ export type GroupRowValueSetterDistributionRecord<TData = any, TValue = any, TCo
 export interface GroupRowValueSetterDistributionOptions {
     /**
      * Distribution strategy to use. See {@link GroupRowValueSetterDistribution} for details.
-     * Set to `'none'`, `false`, or `null` to suppress distribution.
+     * Set to `true` to use the built-in default. Enables custom aggFuncs with `'overwrite'`.
+     * Note: `count`/`min`/`max`/`first`/`last` are only enabled via explicit per-aggFunc record entries.
+     * Useful for overriding `false`/`null` from a parent in deep-merge scenarios.
+     * Set to `false` or `null` to suppress distribution and make the cell not editable.
+     * When `undefined`, inherits from the parent options.
      *
-     * When omitted, defaults to `'uniform'` for `sum`, `'overwrite'` for `avg`/`count`,
-     * and the aggFunc's own strategy for `first`/`last`/`min`/`max`.
+     * When omitted at all levels, defaults to `'uniform'` for `sum`, `'overwrite'` for `avg`
+     * and columns without an aggFunc,
+     * and disabled for `count`/`min`/`max`/`first`/`last` and custom aggFuncs.
      */
-    distribution?: GroupRowValueSetterDistribution | false | null;
+    distribution?: GroupRowValueSetterDistribution | boolean | null;
 
     /**
-     * Number of decimal places to round distributed values to.
-     * Spreads any rounding remainder across children so the total matches exactly.
+     * Number of decimal places to round values written to **child rows** during distribution.
+     * Spreads any rounding remainder across children so their total matches exactly.
      *
      * - `0` — integers (e.g. `10 / 3` → `[4, 3, 3]`)
      * - `2` — two decimals (e.g. `10 / 3` → `[3.34, 3.33, 3.33]`)
@@ -258,14 +275,19 @@ export interface GroupRowValueSetterDistributionOptions {
      *   `cellEditorParams.precision` if set, `0` if `cellEditorParams.step` is a whole number,
      *   no rounding otherwise.
      *
+     * Note: the group row's displayed value is re-computed by the `aggFunc` after distribution.
+     * For `sum`, the sum of rounded children always honours the same precision. For other
+     * aggregation functions like `avg`, the re-aggregated value may not — for example,
+     * the average of integers is not necessarily an integer.
+     *
      * Ignored for `bigint` columns — bigint values are always distributed as integers.
      *
      * @example
      * ```ts
-     * // Integer rounding
+     * // Round child values to integers
      * colDef.groupRowValueSetter = { precision: 0 };
      *
-     * // Currency rounding (2 decimal places)
+     * // Round child values to 2 decimal places (e.g. currency)
      * colDef.groupRowValueSetter = { precision: 2 };
      * ```
      */
@@ -307,11 +329,12 @@ export interface GroupRowValueSetterDistributionOptions {
  * Assign to `colDef.groupRowValueSetter` or use `true` for defaults.
  *
  * **Defaults by aggFunc:**
- * - `sum` → `'uniform'` (divides equally)
- * - `avg` / `count` → `'overwrite'` (writes the edited value to all children)
- * - `min` / `max` → writes to the child holding the extremum
- * - `first` / `last` → writes to that child only
- * - Other aggFuncs → `'overwrite'`
+ * - No aggFunc: `'overwrite'` (writes the edited value to all children)
+ * - `sum`: `'uniform'` (divides equally)
+ * - `avg`: `'overwrite'` (writes the edited value to all children)
+ * - `count` / `min` / `max` / `first` / `last`: disabled by default (cell is not editable unless
+ *   explicitly enabled via a per-aggFunc record entry with `true` or `'overwrite'`)
+ * - Custom aggFuncs: disabled by default (set a `distribution` or use `default` to enable)
  *
  * @example
  * ```ts
@@ -356,7 +379,15 @@ export interface GroupRowValueSetterOptions<TData = any, TValue = any, TContext
      * Distribution strategy or per-aggregation-function strategy map.
      *
      * **As a string:** applies the chosen {@link GroupRowValueSetterDistribution} strategy
-     * to all aggregation functions. Set to `'none'`, `false`, or `null` to suppress distribution.
+     * to all aggregation functions.
+     *
+     * **As `true`:** enables distribution using built-in defaults for distributable aggregation
+     * functions and custom aggFuncs (which get `'overwrite'`).
+     * Note: `count`/`min`/`max`/`first`/`last` are only enabled via explicit per-aggFunc record entries.
+     * Useful for overriding `false`/`null` from `defaultColDef` in deep-merge scenarios.
+     *
+     * **As `false` or `null`:** suppresses distribution and makes the cell not editable
+     * (overriding `groupRowEditable`).
      *
      * **As a record:** maps aggFunc names to individual strategies, options objects,
      * or custom callbacks. Unmatched aggFuncs fall through to {@link default},
@@ -367,29 +398,43 @@ export interface GroupRowValueSetterOptions<TData = any, TValue = any, TContext
      * // Single strategy
      * distribution: 'percentage'
      *
-     * // Per-aggFunc record
-     * distribution: { sum: 'percentage', avg: 'increment', count: false }
+     * // Enable all aggFuncs with built-in defaults
+     * distribution: true
+     *
+     * // Per-aggFunc record (entries can be strings, objects, functions, true, or false/null)
+     * distribution: { sum: 'percentage', avg: 'increment', count: true, myAgg: (params) => { ... }, min: false }
      * ```
      */
     distribution?:
         | GroupRowValueSetterDistribution
-        | false
+        | boolean
         | null
         | GroupRowValueSetterDistributionRecord<TData, TValue, TContext>;
 
     /**
-     * Fallback handler for aggFuncs not listed in the `distribution` record.
-     * Only used when `distribution` is a record — when it is a string or omitted,
-     * the built-in defaults apply and `default` is not called.
+     * Fallback for aggFuncs that don't have a specific distribution strategy.
+     * When `distribution` is a record, applies to aggFuncs not listed in the record.
+     * When `distribution` is omitted, applies only to custom (non-built-in) aggFuncs.
+     * Ignored when `distribution` is a string (all aggFuncs use the specified strategy).
+     *
+     * Accepts the same values as record entries:
+     * - A function for full custom handling.
+     * - A strategy string (e.g. `'overwrite'`).
+     * - `false` or `null` to suppress distribution and make unmatched aggFunc cells not editable.
+     * - An options object with strategy and precision.
      *
      * @example
      * ```ts
+     * // Custom handler
      * default: (params) => {
      *     for (const child of params.aggregatedChildren) {
      *         child.setDataValue(params.column, params.newValue, 'data');
      *     }
      * }
+     *
+     * // Suppress all unmatched aggFuncs
+     * default: false
      * ```
      */
-    default?: GroupRowValueSetterFunc<TData, TValue, TContext>;
+    default?: GroupRowValueSetterDistributionEntry<TData, TValue, TContext>;
 }
diff --git a/packages/ag-grid-community/src/entities/colDef.ts b/packages/ag-grid-community/src/entities/colDef.ts
index 19194ea3cb4..93cb0a29348 100644
--- a/packages/ag-grid-community/src/entities/colDef.ts
+++ b/packages/ag-grid-community/src/entities/colDef.ts
@@ -433,15 +433,21 @@ export interface ColDef<TData = any, TValue = any> extends AbstractColDef<TData,
      */
     editable?: boolean | EditableCallback<TData, TValue>;
     /**
-     * Works like `editable`, but is evaluated only for group rows. When provided, group rows use this property instead of `editable`.
-     * Set to `true` if this column is editable, otherwise `false`. Can also be a function to have different rows editable.
+     * Works like `editable`, but is evaluated only for group rows. When provided, group rows use
+     * this property instead of `editable`. Set to `true` to make group row cells editable, or use
+     * a callback to control editability per row.
      *
      * When `groupRowEditable` is defined and no explicit `groupRowValueSetter` is provided,
      * the built-in {@link distributeGroupValue | distributeGroupValue} is used automatically.
-     * Set `groupRowValueSetter: false` to disable distribution while keeping group rows editable.
      *
-     * @agModule `RowGroupingEditModule`
+     * Columns with `groupRowEditable` or `groupRowValueSetter` do not require `field` or
+     * `valueSetter` — the group row value setter handles the edit entirely.
      *
+     * Note: if `groupRowValueSetter` resolves to `false` or `null` (via `distribution: false`,
+     * a per-aggFunc record entry, or `groupRowValueSetter: false`), the cell is treated as not
+     * editable even when `groupRowEditable` is `true`.
+     *
+     * @agModule `RowGroupingEditModule`
      */
     groupRowEditable?: boolean | GroupRowEditableCallback<TData, TValue>;
     /**
@@ -449,11 +455,14 @@ export interface ColDef<TData = any, TValue = any> extends AbstractColDef<TData,
      *
      * - **`true`**: Uses the built-in {@link distributeGroupValue | distributeGroupValue} with default settings.
      *   Also enabled implicitly when `groupRowEditable` is defined and `groupRowValueSetter` is not set.
-     * - **`false`**: Explicitly disables group row value distribution, even if `groupRowEditable` is defined.
+     * - **`false`**: Explicitly disables group row value distribution and makes the cell not editable,
+     *   even if `groupRowEditable` is defined.
      * - **Function**: A custom callback that receives a {@link GroupRowValueSetterParams} and pushes
-     *   edits down to descendants. The grid always commits the group row value afterwards.
+     *   edits down to descendants. The column does not need `field` or `valueSetter` — the callback
+     *   handles the edit entirely.
      * - **Options object**: Uses the built-in distribution logic with a {@link GroupRowValueSetterOptions}
-     *   configuration.
+     *   configuration. When `distribution` resolves to `false` or `null` for the column's aggFunc,
+     *   the cell is treated as not editable (overriding `groupRowEditable`).
      *
      * Fires for every `setDataValue` call when active, regardless of `groupRowEditable`.
      *
diff --git a/packages/ag-grid-community/src/entities/gridOptions.ts b/packages/ag-grid-community/src/entities/gridOptions.ts
index 364b29d0c20..6ded9438b28 100644
--- a/packages/ag-grid-community/src/entities/gridOptions.ts
+++ b/packages/ag-grid-community/src/entities/gridOptions.ts
@@ -1304,10 +1304,11 @@ export interface GridOptions<TData = any> {
      */
     rowDragManaged?: boolean;
     /**
-     * When `true`, managed row dragging updates grouped column values so rows can move between groups. When `false`,
-     * managed dragging only reorders rows inside their existing group.
+     * When `true`, the grid re-evaluates the grouping hierarchy after editing a grouped column value,
+     * moving the row to the correct group instantly. Also enables managed row dragging to update
+     * grouped column values so rows can move between groups.
      * @default false
-     * @agModule `RowDragModule`
+     * @agModule `RowGroupingModule` / `TreeDataModule`
      */
     refreshAfterGroupEdit?: boolean;
     /**
diff --git a/packages/ag-grid-community/src/focus-overrides.test.ts b/packages/ag-grid-community/src/focus-overrides.test.ts
index 95459f862ee..9ef07e76b7b 100644
--- a/packages/ag-grid-community/src/focus-overrides.test.ts
+++ b/packages/ag-grid-community/src/focus-overrides.test.ts
@@ -1389,12 +1389,24 @@ describe('Focus override callbacks', () => {
 
             headerCtrlAny.onTabKeyDown(event);
 
-            expect(event.defaultPrevented).toBe(true);
+            expect(event.defaultPrevented).toBe(false);
             expect(gridCtrl.focusNextInnerContainer).toHaveBeenCalledTimes(1);
             expect(gridCtrl.focusNextInnerContainer).toHaveBeenCalledWith(true);
             expect(gridCtrl.forceFocusOutOfContainer).toHaveBeenCalledWith(true);
         });
 
+        test('tab from header: unresolved movement forces focus out without preventing default', () => {
+            gridCtrl.focusNextInnerContainer.mockReturnValue(undefined);
+            const event = createTabEvent(false);
+
+            headerCtrlAny.onTabKeyDown(event);
+
+            expect(event.defaultPrevented).toBe(false);
+            expect(gridCtrl.focusNextInnerContainer).toHaveBeenCalledTimes(1);
+            expect(gridCtrl.focusNextInnerContainer).toHaveBeenCalledWith(false);
+            expect(gridCtrl.forceFocusOutOfContainer).toHaveBeenCalledWith(false);
+        });
+
         test('shift-tab from header: successful first move does not trigger fallback call', () => {
             gridCtrl.focusNextInnerContainer.mockReturnValue(true);
             const event = createTabEvent(true);
diff --git a/packages/ag-grid-community/src/headerRendering/gridHeaderCtrl.ts b/packages/ag-grid-community/src/headerRendering/gridHeaderCtrl.ts
index 6c7f30793ae..f99f96be475 100644
--- a/packages/ag-grid-community/src/headerRendering/gridHeaderCtrl.ts
+++ b/packages/ag-grid-community/src/headerRendering/gridHeaderCtrl.ts
@@ -5,6 +5,7 @@ import { _exists } from '../agStack/utils/generic';
 import { BeanStub } from '../context/beanStub';
 import type { BeanCollection } from '../context/context';
 import type { HeaderNavigationDirection } from '../navigation/headerNavigationService';
+import { _focusNextGridCoreContainer } from '../utils/gridFocus';
 import { ManagedFocusFeature } from '../widgets/managedFocusFeature';
 import { getColumnHeaderRowHeight, getFloatingFiltersHeight, getGroupRowsHeight } from './headerUtils';
 
@@ -131,22 +132,11 @@ export class GridHeaderCtrl extends BeanStub {
         const { beans } = this;
         const { headerNavigation, focusSvc } = beans;
 
-        let focused =
-            headerNavigation!.navigateHorizontally(direction, true, e) || (!backwards && focusSvc.focusOverlay(false));
-
-        if (!focused) {
-            const gridCtrl = beans.ctrlsSvc.get('gridCtrl');
-            const focusResult = gridCtrl.focusNextInnerContainer(backwards);
-
-            if (focusResult === true) {
-                focused = true;
-            } else if (focusResult === undefined) {
-                gridCtrl.forceFocusOutOfContainer(backwards);
-                focused = true;
-            }
-        }
-
-        if (focused) {
+        if (
+            headerNavigation!.navigateHorizontally(direction, true, e) ||
+            (!backwards && focusSvc.focusOverlay(false)) ||
+            _focusNextGridCoreContainer(beans, backwards, true)
+        ) {
             // preventDefault so that the tab key doesn't cause focus to get lost
             e.preventDefault();
         }
diff --git a/packages/ag-grid-community/src/interfaces/iRowGroupingEditValueSvc.ts b/packages/ag-grid-community/src/interfaces/iRowGroupingEditValueSvc.ts
index 8bf6c8c7686..03c1cae0b75 100644
--- a/packages/ag-grid-community/src/interfaces/iRowGroupingEditValueSvc.ts
+++ b/packages/ag-grid-community/src/interfaces/iRowGroupingEditValueSvc.ts
@@ -1,8 +1,17 @@
 import type { AgColumn } from '../entities/agColumn';
 import type { RowNode } from '../entities/rowNode';
+import type { IRowNode } from './iRowNode';
 
 /** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */
 export interface IRowGroupingEditValueSvc {
+    /**
+     * Called when `groupRowEditable` is defined on the column.
+     * Evaluates the `groupRowEditable` callback and checks whether the distribution
+     * strategy suppresses the edit (e.g. `distribution: 'none'`).
+     * Returns `true` if editable, `false` otherwise.
+     */
+    isGroupCellEditable(rowNode: IRowNode, column: AgColumn): boolean;
+
     /**
      * Resolves and executes groupRowValueSetter for a group row edit.
      * Returns `undefined` if no groupRowValueSetter is configured (caller should use normal path).
diff --git a/packages/ag-grid-community/src/valueService/valueService.ts b/packages/ag-grid-community/src/valueService/valueService.ts
index ef5bddf07d1..d10ed8467f1 100644
--- a/packages/ag-grid-community/src/valueService/valueService.ts
+++ b/packages/ag-grid-community/src/valueService/valueService.ts
@@ -434,7 +434,7 @@ export class ValueService extends BeanStub implements NamedBean {
             rowNode.data = {}; // enableGroupEdit allows editing group rows without data.
         }
 
-        if (!this.isSetValueSupported({ column, newValue, colDef })) {
+        if (!this.isSetValueSupported(column, rowNode, newValue, colDef)) {
             return false;
         }
 
@@ -568,12 +568,12 @@ export class ValueService extends BeanStub implements NamedBean {
         return true;
     }
 
-    private isSetValueSupported(params: {
-        column: AgColumn;
-        newValue: any;
-        colDef: ReturnType<AgColumn['getColDef']>;
-    }): boolean {
-        const { column, newValue, colDef } = params;
+    private isSetValueSupported(
+        column: AgColumn,
+        rowNode: IRowNode,
+        newValue: any,
+        colDef: ReturnType<AgColumn['getColDef']>
+    ): boolean {
         const { field, valueSetter } = colDef;
 
         const formulaSvc = this.beans.formula;
@@ -581,6 +581,11 @@ export class ValueService extends BeanStub implements NamedBean {
         const hasExternalFormulaData = !!this.formulaDataSvc?.hasDataSource();
 
         if (_missing(field) && _missing(valueSetter) && !(hasExternalFormulaData && isFormulaValue)) {
+            // Group rows with groupRowValueSetter or groupRowEditable don't need field or valueSetter —
+            // the groupRowValueSetter handles the edit entirely.
+            if (rowNode.group && (colDef.groupRowValueSetter || colDef.groupRowEditable)) {
+                return true;
+            }
             _warn(17);
             return false;
         }
diff --git a/packages/ag-grid-community/src/version.ts b/packages/ag-grid-community/src/version.ts
index 320ce849941..ff797fa4963 100644
--- a/packages/ag-grid-community/src/version.ts
+++ b/packages/ag-grid-community/src/version.ts
@@ -1,2 +1,2 @@
 // DO NOT UPDATE MANUALLY: Generated from script during build time
-export const VERSION = '35.1.0-beta.20260320.1044';
+export const VERSION = '35.2.0-beta.20260324.1227';
diff --git a/packages/ag-grid-enterprise/package.json b/packages/ag-grid-enterprise/package.json
index ebd879c0e98..3c3eeecb1b4 100644
--- a/packages/ag-grid-enterprise/package.json
+++ b/packages/ag-grid-enterprise/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-enterprise",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
   "main": "./dist/package/main.cjs.js",
   "types": "./dist/types/src/main.d.ts",
@@ -113,15 +113,15 @@
   ],
   "homepage": "https://www.ag-grid.com/",
   "dependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044"
+    "ag-grid-community": "35.2.0-beta.20260324.1227"
   },
   "optionalDependencies": {
-    "ag-charts-community": "13.1.0-beta.20260320",
-    "ag-charts-enterprise": "13.1.0-beta.20260320"
+    "ag-charts-community": "13.2.0-beta.20260324",
+    "ag-charts-enterprise": "13.2.0-beta.20260324"
   },
   "devDependencies": {
-    "ag-charts-community": "13.1.0-beta.20260320",
-    "ag-charts-enterprise": "13.1.0-beta.20260320",
+    "ag-charts-community": "13.2.0-beta.20260324",
+    "ag-charts-enterprise": "13.2.0-beta.20260324",
     "@types/jest": "^29.5.0",
     "jest": "^29.5.0",
     "jest-environment-jsdom": "^29.7.0",
diff --git a/packages/ag-grid-enterprise/src/aggregation/filterAggregatesStage.ts b/packages/ag-grid-enterprise/src/aggregation/filterAggregatesStage.ts
index 057092ee6a1..7359d13aea3 100644
--- a/packages/ag-grid-enterprise/src/aggregation/filterAggregatesStage.ts
+++ b/packages/ag-grid-enterprise/src/aggregation/filterAggregatesStage.ts
@@ -61,6 +61,7 @@ export class FilterAggregatesStage extends BeanStub implements NamedBean, _IRowN
                     setAllChildrenCount(node);
                 } else {
                     node.setAllChildrenCount(null);
+                    node.pinnedSibling?.setAllChildrenCount(null);
                 }
             }
 
@@ -89,6 +90,7 @@ export class FilterAggregatesStage extends BeanStub implements NamedBean, _IRowN
                 setAllChildrenCount(node);
             } else {
                 node.setAllChildrenCount(null);
+                node.pinnedSibling?.setAllChildrenCount(null);
             }
             if (node.sibling) {
                 node.sibling.childrenAfterAggFilter = node.childrenAfterAggFilter;
@@ -114,12 +116,13 @@ export class FilterAggregatesStage extends BeanStub implements NamedBean, _IRowN
                 allChildrenCount += childrenAfterAggFilter[i].allChildrenCount ?? 0; // include children of children
             }
         }
-        rowNode.setAllChildrenCount(
+        const count =
             // Maintain the historical behaviour:
             // - allChildrenCount is 0 in the root if there are no children
             // - allChildrenCount is null in any non-root row if there are no children
-            allChildrenCount === 0 && rowNode.level >= 0 ? null : allChildrenCount
-        );
+            allChildrenCount === 0 && rowNode.level >= 0 ? null : allChildrenCount;
+        rowNode.setAllChildrenCount(count);
+        rowNode.pinnedSibling?.setAllChildrenCount(count);
     };
 
     /* for grid data, we only count the leafs */
@@ -135,5 +138,6 @@ export class FilterAggregatesStage extends BeanStub implements NamedBean, _IRowN
             }
         }
         rowNode.setAllChildrenCount(allChildrenCount);
+        rowNode.pinnedSibling?.setAllChildrenCount(allChildrenCount);
     };
 }
diff --git a/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/cartesianChartProxy.ts b/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/cartesianChartProxy.ts
index a4b1f9015ec..1f90c08dcdf 100644
--- a/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/cartesianChartProxy.ts
+++ b/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/cartesianChartProxy.ts
@@ -32,6 +32,7 @@ export abstract class CartesianChartProxy<
 > extends ChartProxy<AgCartesianChartOptions, TSeries> {
     protected crossFilteringAllPoints = new Set<string>();
     protected crossFilteringSelectedPoints: string[] = [];
+    protected isSingleSeries: boolean = false;
 
     protected abstract getAxes(
         params: UpdateParams,
@@ -241,7 +242,7 @@ export abstract class CartesianChartProxy<
     protected override getSeriesChartThemeDefaults(): AgChartThemeOverrides[TSeries] {
         return {
             series: {
-                highlight: getSeriesHighlight(this.crossFiltering),
+                highlight: getSeriesHighlight(this.crossFiltering, this.isSingleSeries),
             },
         };
     }
diff --git a/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/waterfallChartProxy.ts b/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/waterfallChartProxy.ts
index 59d96f6c966..bc5ce65621f 100644
--- a/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/waterfallChartProxy.ts
+++ b/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/cartesian/waterfallChartProxy.ts
@@ -4,6 +4,8 @@ import type { UpdateParams } from '../chartProxy';
 import { CartesianChartProxy } from './cartesianChartProxy';
 
 export class WaterfallChartProxy extends CartesianChartProxy<'waterfall'> {
+    protected override isSingleSeries: boolean = true;
+
     protected override getAxes(
         params: UpdateParams,
         commonChartOptions: AgCartesianChartOptions
diff --git a/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/chartTheme.ts b/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/chartTheme.ts
index 81c0e71dbde..59c0c39c167 100644
--- a/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/chartTheme.ts
+++ b/packages/ag-grid-enterprise/src/charts/chartComp/chartProxies/chartTheme.ts
@@ -282,16 +282,19 @@ export function lookupCustomChartTheme(chartProxyParams: ChartProxyParams, name:
 }
 
 export function getSeriesHighlight(
-    crossFiltering: boolean
+    crossFiltering: boolean,
+    isSingleSeries?: boolean
 ): AgMultiSeriesHighlightOptions<AgBaseHighlightStyleOptions> {
     const highlight: AgMultiSeriesHighlightOptions<AgBaseHighlightStyleOptions> = {
-        unhighlightedSeries: {
-            opacity: 1,
-        },
         highlightedItem: {
             strokeWidth: 2,
         },
     };
+    if (!isSingleSeries) {
+        highlight.unhighlightedSeries = {
+            opacity: 1,
+        };
+    }
     if (!crossFiltering) {
         highlight.unhighlightedItem = {
             opacity: 1,
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts b/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts
index 8f2ff1b7356..a9ffd0fbffb 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts
@@ -176,7 +176,7 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
                 getCurrentDragValue: (listItemDragStartEvent: ColumnPanelItemDragStartEvent) =>
                     getCurrentDragValue(listItemDragStartEvent),
                 isMoveBlocked: (currentDragValue: AgColumn | AgProvidedColumnGroup | null) =>
-                    isMoveBlocked(gos, beans, getCurrentColumnsBeingMoved(currentDragValue)),
+                    isMoveBlocked(gos, beans, getCurrentColumnsBeingMoved(currentDragValue), this.params),
                 getNumRows: (comp: AgPrimaryColsList) => comp.getDisplayedColsList().length,
                 moveItem: (
                     currentDragValue: AgColumn | AgProvidedColumnGroup | null,
@@ -192,7 +192,7 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
         const { group, columnGroup, column, expanded } = modelItem;
         const currentColumns = getCurrentColumnsBeingMoved(group ? columnGroup : column);
 
-        if (isMoveBlocked(gos, beans, currentColumns)) {
+        if (isMoveBlocked(gos, beans, currentColumns, this.params)) {
             return;
         }
 
@@ -263,7 +263,10 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
 
         const pivotModeActive = this.colModel.isPivotMode();
         const deferApply = isDeferredMode(params);
-        const shouldSyncColumnLayoutWithGrid = (!params.suppressSyncLayoutWithGrid || deferApply) && !pivotModeActive;
+        const hasDeferredColumnOrder =
+            deferApply && this.beans.columnStateUpdateStrategy.hasDeferredColumnOrder(deferApply);
+        const shouldSyncColumnLayoutWithGrid =
+            ((!params.suppressSyncLayoutWithGrid || deferApply) && !pivotModeActive) || hasDeferredColumnOrder;
 
         if (shouldSyncColumnLayoutWithGrid) {
             this.buildTreeFromWhatGridIsDisplaying();
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts b/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts
index a0f7ee46572..09b2ed4b24c 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts
@@ -53,8 +53,14 @@ const getMoveDiff = (allColumns: AgColumn[], currentColumns: AgColumn[] | null,
     return 0;
 };
 
-export const isMoveBlocked = (gos: GridOptionsService, beans: BeanCollection, currentColumns: AgColumn[]): boolean => {
-    const preventMoving = gos.get('suppressMovableColumns') || beans.colModel.isPivotMode();
+export const isMoveBlocked = (
+    gos: GridOptionsService,
+    beans: BeanCollection,
+    currentColumns: AgColumn[],
+    params: ColumnStateUpdateParams
+): boolean => {
+    const deferMode = isDeferredMode(params);
+    const preventMoving = gos.get('suppressMovableColumns') || beans.columnStateUpdateStrategy.getPivotMode(deferMode);
 
     if (preventMoving) {
         return true;
diff --git a/packages/ag-grid-enterprise/src/license/shared/licenseManager.ts b/packages/ag-grid-enterprise/src/license/shared/licenseManager.ts
index baa5dd88b28..f8736c37226 100644
--- a/packages/ag-grid-enterprise/src/license/shared/licenseManager.ts
+++ b/packages/ag-grid-enterprise/src/license/shared/licenseManager.ts
@@ -15,7 +15,7 @@ export interface ILicenseManager {
 
 export class LicenseManager {
     // eslint-disable-next-line no-restricted-syntax
-    private static readonly RELEASE_INFORMATION: string = 'MTc3MDgwOTgzNDk5MQ==';
+    private static readonly RELEASE_INFORMATION: string = 'MTc3NDAxNzg5NzAyNg==';
     // eslint-disable-next-line no-restricted-syntax
     private static licenseKey: string;
     // eslint-disable-next-line no-restricted-syntax
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts
index dceb5b89def..262007b38be 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts
@@ -90,13 +90,14 @@ export abstract class BaseDropZonePanel extends PillDropZonePanel<DropZoneColumn
         if (!columns) {
             return;
         }
+        // In deferred mode, skip visibility changes from drag-and-drop — they will be
+        // applied when the deferred state is committed. Creating hide patches here leaves
+        // stale state when a column is removed then re-added to row groups.
+        if (isDeferredMode(this.updateParams)) {
+            return;
+        }
         const allowedCols = columns.filter((c) => !c.getColDef().lockVisible);
-        this.beans.columnStateUpdateStrategy.setColumnsVisible(
-            isDeferredMode(this.updateParams),
-            allowedCols,
-            visible,
-            source
-        );
+        this.beans.columnStateUpdateStrategy.setColumnsVisible(false, allowedCols, visible, source);
     }
 
     private isRowGroupPanel() {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts
index fc296910511..7cf21f6522f 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts
@@ -360,11 +360,14 @@ export class DropZoneColumnComp extends PillDragComp<AgColumn> {
                 value,
                 'toolPanelDragAndDrop'
             );
-            const eText = this.getGui().querySelector<HTMLElement>('.ag-column-drop-cell-text');
-            if (eText) {
-                eText.textContent = this.getDisplayValue();
+            // In synchronous mode, setColumnAggFunc dispatches events that can destroy this component, nulling this.column
+            if (this.column) {
+                const eText = this.getGui().querySelector<HTMLElement>('.ag-column-drop-cell-text');
+                if (eText) {
+                    eText.textContent = this.getDisplayValue();
+                }
+                this.setupAria();
             }
-            this.setupAria();
             refreshDeferredToolPanelUi(this.beans, this.updateParams);
         };
 
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributeGroupValue.ts b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributeGroupValue.ts
index cc7b545fd36..4364577434d 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributeGroupValue.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributeGroupValue.ts
@@ -1,18 +1,21 @@
 import type {
+    GroupRowValueSetterDistribution,
+    GroupRowValueSetterDistributionEntry,
     GroupRowValueSetterDistributionOptions,
-    GroupRowValueSetterFunc,
     GroupRowValueSetterOptions,
     GroupRowValueSetterParams,
 } from 'ag-grid-community';
 
 import { DistributorBigInt } from './distributorBigInt';
 import { DistributorNumber } from './distributorNumber';
+import type { AggFuncInput } from './valueConversion';
+import { isDistributableBuiltin, isNonDistributable } from './valueConversion';
 
 /**
  * Built-in `groupRowValueSetter` that distributes a group-level value edit
  * down to descendant rows, respecting the column's aggregation function.
  *
- * Assign directly for default behaviour (uniform for sum, overwrite for avg/others):
+ * Assign directly for default behaviour (uniform for sum, overwrite for avg/no-aggFunc):
  * ```ts
  * colDef.groupRowValueSetter = distributeGroupValue;
  * ```
@@ -34,63 +37,134 @@ export const distributeGroupValue = (
         return false;
     }
 
-    const aggFunc = typeof colDef.aggFunc === 'string' ? colDef.aggFunc : null;
+    const aggFunc = colDef.aggFunc ?? null;
+    const entry = resolveDistributionEntry(options, aggFunc);
 
-    // Resolve distribution options, handling per-aggFunc records
-    let entry: GroupRowValueSetterDistributionOptions | undefined;
-    let defaultHandler: GroupRowValueSetterFunc | undefined;
-    if (options) {
-        const dist = options.distribution;
-        defaultHandler = options.default;
+    if (entry === false) {
+        return false;
+    }
+    if (typeof entry === 'function') {
+        return entry(params) ?? true;
+    }
 
-        // Explicit suppression at top level
-        if (dist === false || dist === null) {
+    // Delegate to the type-appropriate distributor
+    if (colDef.cellDataType === 'bigint') {
+        return new DistributorBigInt(params, entry, aggFunc).run();
+    }
+    return new DistributorNumber(params, entry, aggFunc).run();
+};
+
+/** Resolved distribution entry: options for distributors, a custom handler, or `false` (suppressed). */
+type ResolvedEntry = GroupRowValueSetterDistributionOptions | ((...args: any[]) => any) | false | undefined;
+
+/**
+ * Resolves the distribution entry from user options, handling per-aggFunc records and default fallbacks.
+ *
+ * Resolution order for per-aggFunc records:
+ *   1. Explicit entry for the column's aggFunc → resolveEntry
+ *   2. `options.default` handler (for unlisted aggFuncs)
+ *   3. Inherit from parent options (uses built-in defaults via resolveStrategy)
+ *
+ * Non-distributable aggFuncs (count/min/max/first/last) are disabled unless
+ * explicitly enabled with 'overwrite' (top-level or per-aggFunc record).
+ */
+export function resolveDistributionEntry(
+    options: GroupRowValueSetterOptions | undefined,
+    aggFunc: AggFuncInput
+): ResolvedEntry {
+    if (!options) {
+        return undefined;
+    }
+
+    const dist = options.distribution;
+
+    if (dist === false || dist === null) {
+        return false;
+    }
+
+    const nonDist = isNonDistributable(aggFunc);
+    const defaultHandler = options.default;
+
+    // Per-aggFunc record — look up the entry, then fall back to default handler or inherit
+    if (typeof dist === 'object') {
+        const aggEntry = typeof aggFunc === 'string' ? dist[aggFunc] : undefined;
+        if (aggEntry !== undefined) {
+            return resolveEntry(aggEntry, nonDist, options);
+        }
+        // Not listed: non-distributable → disabled, others → default handler or inherit
+        if (nonDist) {
             return false;
         }
+        return resolveEntry(defaultHandler, false, options) ?? inheritOptions(options);
+    }
+
+    // Top-level (undefined, true, or strategy string)
+    if (nonDist) {
+        return dist === 'overwrite' ? inheritOptions(options, 'overwrite') : false;
+    }
+
+    // Custom/unknown aggFuncs: check default handler before falling through to built-in defaults
+    if (dist === undefined && !isDistributableBuiltin(aggFunc)) {
+        const resolved = resolveEntry(defaultHandler, false, options);
+        if (resolved !== undefined) {
+            return resolved;
+        }
+    }
+
+    return options as GroupRowValueSetterDistributionOptions;
+}
 
-        if (dist === undefined || typeof dist === 'string') {
-            // Simple string or undefined distribution — pass options directly (extra properties are ignored)
-            entry = options as GroupRowValueSetterDistributionOptions;
-        } else {
-            // Per-aggFunc record — look up the entry for the current aggFunc
-            const perAgg = aggFunc != null ? dist[aggFunc] : undefined;
-
-            // Explicit suppression for this aggFunc
-            if (perAgg === false || perAgg === null) {
-                return false;
-            }
-
-            if (typeof perAgg === 'function') {
-                return perAgg(params) ?? true;
-            }
-            const { precision, getValue, setValue } = options;
-            if (typeof perAgg === 'string') {
-                entry = { distribution: perAgg, precision, getValue, setValue };
-            } else if (perAgg != null) {
-                entry = {
-                    distribution: perAgg.distribution,
-                    precision: perAgg.precision ?? precision,
-                    getValue: perAgg.getValue ?? getValue,
-                    setValue: perAgg.setValue ?? setValue,
-                };
-            } else {
-                // aggFunc not in the record — fall through to default handler or overwrite
-                if (defaultHandler) {
-                    return defaultHandler(params) ?? true;
-                }
-                entry = { distribution: 'overwrite', precision, getValue, setValue };
-            }
-            // Record mode handles unmatched aggFuncs above — the distributor doesn't need defaultHandler
-            defaultHandler = undefined;
+/**
+ * Normalizes a single distribution entry (from a per-aggFunc record or default handler),
+ * inheriting precision/getValue/setValue from the parent options.
+ *
+ * For non-distributable aggFuncs, only 'overwrite', true, or a custom function are accepted;
+ * all other strategies return false (disabled).
+ */
+function resolveEntry(
+    entry: GroupRowValueSetterDistributionEntry | undefined,
+    nonDist: boolean,
+    parent: GroupRowValueSetterOptions
+): ResolvedEntry {
+    if (entry === false || entry === null) {
+        return false;
+    }
+    if (entry === undefined) {
+        return undefined;
+    }
+    if (typeof entry === 'function') {
+        return entry;
+    }
+
+    // Extract the distribution value: from object property or the entry itself (string/true)
+    if (typeof entry === 'object') {
+        const dist = entry.distribution;
+
+        // Non-distributable: only 'overwrite' or true enables them
+        if (nonDist && dist !== 'overwrite' && dist !== true) {
+            return false;
         }
+
+        const { precision: pp, getValue: pgv, setValue: psv } = parent;
+        return {
+            distribution: nonDist ? 'overwrite' : dist,
+            precision: entry.precision ?? pp,
+            getValue: entry.getValue ?? pgv,
+            setValue: entry.setValue ?? psv,
+        };
     }
 
-    // Delegate to the type-appropriate distributor.
-    // Each distributor resolves its own default strategy from the aggFunc.
-    // This check works for both explicit and inferred types — dataTypeService writes the
-    // inferred cellDataType back to colDef before the grid becomes interactive.
-    if (colDef.cellDataType === 'bigint') {
-        return new DistributorBigInt(params, entry, aggFunc, defaultHandler).run();
+    // entry is a strategy string or true
+    if (nonDist && entry !== 'overwrite' && entry !== true) {
+        return false;
     }
-    return new DistributorNumber(params, entry, aggFunc, defaultHandler).run();
-};
+    return inheritOptions(parent, nonDist ? 'overwrite' : entry);
+}
+
+/** Creates distribution options inheriting precision/getValue/setValue from the parent. */
+function inheritOptions(
+    parent: GroupRowValueSetterOptions,
+    distribution?: GroupRowValueSetterDistribution | boolean | null
+): GroupRowValueSetterDistributionOptions {
+    return { distribution, precision: parent.precision, getValue: parent.getValue, setValue: parent.setValue };
+}
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorBigInt.ts b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorBigInt.ts
index dcd952ce9b2..ba3f1e02036 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorBigInt.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorBigInt.ts
@@ -3,12 +3,11 @@ import type {
     DistributionGetValueParams,
     DistributionSetValueParams,
     GroupRowValueSetterDistributionOptions,
-    GroupRowValueSetterFunc,
     GroupRowValueSetterParams,
     IRowNode,
 } from 'ag-grid-community';
 
-import type { DistributionStrategy } from './valueConversion';
+import type { AggFuncInput, DistributionStrategy } from './valueConversion';
 import { isNumericLike, resolveStrategy, toBigInt } from './valueConversion';
 
 /** Distributes a BigInt value to children using integer-safe arithmetic. */
@@ -27,8 +26,7 @@ export class DistributorBigInt {
     constructor(
         private readonly params: GroupRowValueSetterParams,
         opts: GroupRowValueSetterDistributionOptions | undefined,
-        aggFunc: string | null,
-        private readonly defaultHandler: GroupRowValueSetterFunc | undefined
+        aggFunc: AggFuncInput
     ) {
         const { aggregatedChildren: children, column, newValue } = params;
         const newBigInt = toBigInt(newValue);
@@ -55,34 +53,15 @@ export class DistributorBigInt {
     run(): boolean {
         const { strategy, newValue } = this;
 
-        // Explicit 'none' — suppress distribution
-        if (strategy === 'none') {
+        // Explicit suppression
+        if (strategy === false) {
             return false;
         }
 
-        // Unknown aggFunc with no matching strategy — use default handler or overwrite
-        if (strategy === null) {
-            const handler = this.defaultHandler;
-            if (handler) {
-                return handler(this.params) ?? true;
-            }
+        if (strategy === 'overwrite') {
             return this.writeAll(newValue);
         }
 
-        // Single-child or overwrite strategies — write the raw value
-        switch (strategy) {
-            case 'first':
-                return this.writeOne(0, newValue);
-            case 'last':
-                return this.writeOne(this.count - 1, newValue);
-            case 'min':
-                return this.writeToExtremum(true);
-            case 'max':
-                return this.writeToExtremum(false);
-            case 'overwrite':
-                return this.writeAll(newValue);
-        }
-
         // Non-numeric value (e.g. null, non-numeric string) — write raw value to all children
         const { target, oldTarget } = this;
         if (target === 0n && !isNumericLike(newValue)) {
@@ -139,21 +118,6 @@ export class DistributorBigInt {
         return changed;
     }
 
-    /** Writes the new value to the child currently holding the min or max. */
-    private writeToExtremum(isMin: boolean): boolean {
-        const { count, newValue } = this;
-        let targetIdx = 0;
-        let targetVal = this.readOne(0);
-        for (let i = 1; i < count; i++) {
-            const v = this.readOne(i);
-            if (isMin ? v < targetVal : v > targetVal) {
-                targetVal = v;
-                targetIdx = i;
-            }
-        }
-        return this.writeOne(targetIdx, newValue);
-    }
-
     /** Writes uniform values directly without array allocation. */
     private writeUniformDirect(total: bigint): boolean {
         const { count, bigCount } = this;
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts
index 6856225c076..e2ff16a9889 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts
@@ -3,12 +3,11 @@ import type {
     DistributionGetValueParams,
     DistributionSetValueParams,
     GroupRowValueSetterDistributionOptions,
-    GroupRowValueSetterFunc,
     GroupRowValueSetterParams,
     IRowNode,
 } from 'ag-grid-community';
 
-import type { DistributionStrategy } from './valueConversion';
+import type { AggFuncInput, DistributionStrategy } from './valueConversion';
 import { detectPrecision, isNumericLike, resolveStrategy, toNumber } from './valueConversion';
 
 interface ValueAndCount {
@@ -33,8 +32,7 @@ export class DistributorNumber {
     constructor(
         private readonly params: GroupRowValueSetterParams,
         opts: GroupRowValueSetterDistributionOptions | undefined,
-        aggFunc: string | null,
-        private readonly defaultHandler: GroupRowValueSetterFunc | undefined
+        aggFunc: AggFuncInput
     ) {
         const { aggregatedChildren: children, column, colDef, newValue } = params;
         const newNumber = toNumber(newValue);
@@ -72,32 +70,14 @@ export class DistributorNumber {
     run(): boolean {
         const { strategy, newValue } = this;
 
-        if (strategy === 'none') {
+        if (strategy === false) {
             return false;
         }
 
-        if (strategy === null) {
-            const handler = this.defaultHandler;
-            if (handler) {
-                return handler(this.params) ?? true;
-            }
+        if (strategy === 'overwrite') {
             return this.writeAll(newValue);
         }
 
-        const { children } = this;
-        switch (strategy) {
-            case 'first':
-                return this.writeOne(children[0], newValue);
-            case 'last':
-                return this.writeOne(children[this.count - 1], newValue);
-            case 'min':
-                return this.writeToExtremum(true);
-            case 'max':
-                return this.writeToExtremum(false);
-            case 'overwrite':
-                return this.writeAll(newValue);
-        }
-
         // Non-numeric value (e.g. null, non-numeric string) — write raw value to all children
         if (this.target === 0 && !isNumericLike(newValue)) {
             return this.writeAll(newValue);
@@ -172,21 +152,6 @@ export class DistributorNumber {
         return changed;
     }
 
-    private writeToExtremum(isMin: boolean): boolean {
-        const { children, count, newValue } = this;
-        let bestNode = children[0];
-        let bestVal = this.readOne(bestNode);
-        for (let i = 1; i < count; i++) {
-            const node = children[i];
-            const v = this.readOne(node);
-            if (isMin ? v < bestVal : v > bestVal) {
-                bestVal = v;
-                bestNode = node;
-            }
-        }
-        return this.writeOne(bestNode, newValue);
-    }
-
     private distributeUniform(): boolean {
         const { children, count, target, precision } = this;
         if (precision === undefined) {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/valueConversion.ts b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/valueConversion.ts
index 2937326148e..c083239eaf4 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/valueConversion.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/valueConversion.ts
@@ -1,42 +1,59 @@
 import type { ColDef, GroupRowValueSetterDistribution } from 'ag-grid-community';
 
-/** Resolved distribution strategy. `null` means no strategy — use default handler or overwrite. */
-export type DistributionStrategy = 'first' | 'last' | 'min' | 'max' | GroupRowValueSetterDistribution | null;
+/** Resolved distribution strategy. `false` means suppressed (disabled by default for count/min/max/first/last/custom aggFuncs, or explicit false/null). */
+export type DistributionStrategy = GroupRowValueSetterDistribution | false;
+
+/** The raw aggFunc value from colDef, passed through without coercion.
+ * String = named aggFunc, function = inline custom aggFunc, null/undefined = no aggFunc. */
+export type AggFuncInput = string | ((...args: any[]) => any) | null | undefined;
 
 /**
  * Resolves the distribution strategy from the aggFunc and explicit distribution option.
- * 'none', false, and null always suppress distribution, overriding even first/last/min/max.
- * first/last/min/max aggFuncs use their own strategy unless explicitly overridden.
+ * false and null always suppress distribution.
+ * count/min/max/first/last are non-distributable: only 'overwrite' enables them
+ * (and only via explicit per-aggFunc record entries — see resolveDistributionEntry).
+ * Custom string aggFuncs and function aggFuncs are disabled by default.
+ * Columns with no aggFunc (null/undefined) default to 'overwrite'.
  */
 export const resolveStrategy = (
-    aggFunc: string | null,
-    distribution: GroupRowValueSetterDistribution | false | null | undefined
+    aggFunc: AggFuncInput,
+    distribution: GroupRowValueSetterDistribution | boolean | null | undefined
 ): DistributionStrategy => {
     // Explicit suppression always wins
-    if (distribution === 'none' || distribution === false || distribution === null) {
-        return 'none';
+    if (distribution === false || distribution === null) {
+        return false;
     }
-    switch (aggFunc) {
-        case 'first':
-        case 'last':
-        case 'min':
-        case 'max':
-            return aggFunc;
+    // Non-distributable aggFuncs: only 'overwrite' is valid.
+    // All other strategies (uniform, percentage, increment, true) are invalid → disabled.
+    if (isNonDistributable(aggFunc)) {
+        return distribution === 'overwrite' ? 'overwrite' : false;
     }
-    if (distribution) {
+    // Explicit strategy string — use it for any distributable aggFunc
+    if (typeof distribution === 'string') {
         return distribution;
     }
-    switch (aggFunc) {
-        case 'sum':
-            return 'uniform';
-        case 'avg':
-        case 'count':
-            return 'overwrite';
-        default:
-            return null;
+    // Built-in defaults: sum → uniform, avg/no-aggFunc → overwrite
+    if (aggFunc === 'sum') {
+        return 'uniform';
+    }
+    if (aggFunc === 'avg' || aggFunc == null) {
+        return 'overwrite';
     }
+    // Custom string aggFuncs and function aggFuncs: disabled unless distribution === true
+    return distribution === true ? 'overwrite' : false;
 };
 
+/** Whether the aggFunc is non-distributable: count/min/max/first/last have no meaningful
+ * distribution and are disabled by default. Only explicit 'overwrite' enables them
+ * (at top level or in per-aggFunc record entries; true in per-aggFunc records also works). */
+export const isNonDistributable = (aggFunc: AggFuncInput): boolean =>
+    aggFunc === 'count' || aggFunc === 'min' || aggFunc === 'max' || aggFunc === 'first' || aggFunc === 'last';
+
+/** Whether the aggFunc is a built-in distributable aggFunc with a known default strategy.
+ * sum defaults to 'uniform', avg defaults to 'overwrite'. Custom, no-aggFunc, and
+ * non-distributable aggFuncs return false and should check the options.default handler. */
+export const isDistributableBuiltin = (aggFunc: AggFuncInput): boolean => aggFunc === 'sum' || aggFunc === 'avg';
+
 /** Coerces an unknown value to a number. Returns 0 for non-convertible inputs. Preserves NaN, Infinity, and -Infinity for number inputs. */
 export const toNumber = (raw: unknown): number => {
     if (typeof raw === 'number') {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditModule.ts b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditModule.ts
index dfab5377dfd..2c74b6bd9ac 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditModule.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditModule.ts
@@ -1,75 +1,16 @@
-import type {
-    AgColumn,
-    GroupRowValueSetterParams,
-    NamedBean,
-    RowNode,
-    _IRowGroupingEditValueSvc,
-    _ModuleWithoutApi,
-} from 'ag-grid-community';
-import { BeanStub } from 'ag-grid-community';
+import type { _ModuleWithoutApi } from 'ag-grid-community';
 
 import { EnterpriseCoreModule } from '../agGridEnterpriseModule';
 import { VERSION } from '../version';
-import { distributeGroupValue } from './distributeGroupValue/distributeGroupValue';
+import { RowGroupingEditValueSvc } from './rowGroupingEditValueSvc';
 import { SharedRowGroupingModule } from './rowGroupingModule';
 
-class RowGroupingEditValueSvc extends BeanStub implements NamedBean, _IRowGroupingEditValueSvc {
-    beanName = 'rowGroupingEditValueSvc' as const;
-
-    public setGroupDataValue(
-        rowNode: RowNode,
-        column: AgColumn,
-        newValue: unknown,
-        oldValue: unknown,
-        eventSource: string | undefined,
-        valueChanged: boolean
-    ): boolean | undefined {
-        const colDef = column.getColDef();
-
-        // Resolve groupRowValueSetter: true or groupRowEditable → built-in distributeGroupValue,
-        // false → explicitly disabled, function/object → as-is.
-        // colDef is already deep-merged with defaultColDef (via _mergeDeep in columnFactoryUtils),
-        // so object-type options inherit and merge with defaultColDef automatically.
-        // When groupRowEditable is a callback, evaluate it against the current row — only enable
-        // implicit distribution for rows where the callback returns true.
-        let raw = colDef.groupRowValueSetter;
-        if (raw == null) {
-            const gre = colDef.groupRowEditable;
-            raw = gre && column.isColumnFunc(rowNode, gre) ? true : undefined;
-        }
-        if (!raw) {
-            return undefined; // No groupRowValueSetter or false → caller uses normal path.
-        }
-
-        const params: GroupRowValueSetterParams = this.gos.addCommon({
-            node: rowNode,
-            data: rowNode.data,
-            oldValue,
-            newValue,
-            colDef,
-            column,
-            eventSource,
-            valueChanged,
-            aggregatedChildren: this.beans.aggChildrenSvc?.getAggregatedChildren(rowNode, column) ?? [],
-        });
-
-        const result =
-            typeof raw === 'function'
-                ? raw(params)
-                : // true or options object — delegate to the built-in distributor.
-                  distributeGroupValue(params, raw === true ? undefined : raw);
-
-        // Default to true if user forgot to return a value (possible without TypeScript).
-        return result ?? true;
-    }
-}
-
 /**
  * @feature Editing -> Group Row Edit
  * Enables `groupRowEditable` and `groupRowValueSetter` on group rows.
  * When `groupRowEditable` is set but no `groupRowValueSetter` is provided,
- * the module supplies a default that distributes the edited value to
- * descendant rows using {@link distributeGroupValue}.
+ * the module supplies the builtin that distributes the edited value to
+ * descendant rows.
  */
 export const RowGroupingEditModule: _ModuleWithoutApi = {
     moduleName: 'RowGroupingEdit',
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditValueSvc.ts b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditValueSvc.ts
new file mode 100644
index 00000000000..f9221217cbf
--- /dev/null
+++ b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingEditValueSvc.ts
@@ -0,0 +1,92 @@
+import type {
+    AgColumn,
+    GroupRowValueSetterParams,
+    IRowNode,
+    NamedBean,
+    RowNode,
+    _IRowGroupingEditValueSvc,
+} from 'ag-grid-community';
+import { BeanStub } from 'ag-grid-community';
+
+import { distributeGroupValue, resolveDistributionEntry } from './distributeGroupValue/distributeGroupValue';
+import { resolveStrategy } from './distributeGroupValue/valueConversion';
+
+export class RowGroupingEditValueSvc extends BeanStub implements NamedBean, _IRowGroupingEditValueSvc {
+    beanName = 'rowGroupingEditValueSvc' as const;
+
+    public isGroupCellEditable(rowNode: IRowNode, column: AgColumn): boolean {
+        const colDef = column.getColDef();
+
+        if (!column.isColumnFunc(rowNode, colDef.groupRowEditable)) {
+            return false;
+        }
+
+        const setter = colDef.groupRowValueSetter;
+
+        // Function setter: always editable (user handles distribution)
+        if (typeof setter === 'function') {
+            return true;
+        }
+
+        const aggFunc = colDef.aggFunc ?? null;
+
+        // Options object: resolve per-aggFunc records and defaults, then check strategy
+        if (typeof setter === 'object') {
+            const entry = resolveDistributionEntry(setter, aggFunc);
+            return (
+                typeof entry === 'function' ||
+                (entry !== false && resolveStrategy(aggFunc, entry?.distribution) !== false)
+            );
+        }
+
+        // No setter or true: check if the aggFunc's default strategy is enabled
+        return resolveStrategy(aggFunc, setter ?? undefined) !== false;
+    }
+
+    public setGroupDataValue(
+        rowNode: RowNode,
+        column: AgColumn,
+        newValue: unknown,
+        oldValue: unknown,
+        eventSource: string | undefined,
+        valueChanged: boolean
+    ): boolean | undefined {
+        const colDef = column.getColDef();
+
+        // Resolve groupRowValueSetter: true or groupRowEditable → built-in distributeGroupValue,
+        // false → explicitly disabled, function/object → as-is.
+        // colDef is already deep-merged with defaultColDef (via _mergeDeep in columnFactoryUtils),
+        // so object-type options inherit and merge with defaultColDef automatically.
+        // When groupRowEditable is a callback, evaluate it against the current row — only enable
+        // implicit distribution for rows where the callback returns true.
+        let setter = colDef.groupRowValueSetter;
+        if (setter == null) {
+            const gre = colDef.groupRowEditable;
+            setter = gre && column.isColumnFunc(rowNode, gre) ? true : undefined;
+        }
+        if (!setter) {
+            return undefined; // No groupRowValueSetter or false → caller uses normal path.
+        }
+
+        const params: GroupRowValueSetterParams = this.gos.addCommon({
+            node: rowNode,
+            data: rowNode.data,
+            oldValue,
+            newValue,
+            colDef,
+            column,
+            eventSource,
+            valueChanged,
+            aggregatedChildren: this.beans.aggChildrenSvc?.getAggregatedChildren(rowNode, column) ?? [],
+        });
+
+        const result =
+            typeof setter === 'function'
+                ? setter(params)
+                : // true or options object — delegate to the built-in distributor.
+                  distributeGroupValue(params, setter === true ? undefined : setter);
+
+        // Default to true if user forgot to return a value (possible without TypeScript).
+        return result ?? true;
+    }
+}
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts
index cc2c51c44ee..0435cd4e79a 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts
@@ -2,12 +2,12 @@ import type { AgColumn, BeanCollection, ColumnModel, LocaleTextFunc, RowNode } f
 
 import { setAggData } from '../aggregation/aggDataUtils';
 
-export function setRowNodeGroupValue(
+export const setRowNodeGroupValue = (
     rowNode: RowNode,
     colModel: ColumnModel,
     colKey: string | AgColumn,
     newValue: any
-): void {
+): void => {
     const column = colModel.getCol(colKey)!;
 
     let groupData = rowNode._groupData;
@@ -25,31 +25,38 @@ export function setRowNodeGroupValue(
 
     groupData[columnId] = newValue;
     rowNode.dispatchCellChangedEvent(column, newValue, oldValue);
-}
+};
 
-export function setRowNodeGroup(rowNode: RowNode, beans: BeanCollection, group: boolean): void {
-    if (rowNode.group === group) {
+const doSetRowNodeGroup = (rowNode: RowNode | null | undefined, beans: BeanCollection, group: boolean): void => {
+    if (!rowNode) {
         return;
     }
-
-    // if we used to be a group, and no longer, then close the node
-    if (rowNode.group && !group) {
-        // Clear stale aggData when demoting from group to leaf.
-        const colModel = beans.colModel;
-        setAggData(rowNode, null, colModel);
-        const pinnedSibling = rowNode.pinnedSibling;
-        if (pinnedSibling) {
-            setAggData(pinnedSibling, null, colModel);
-        }
+    const oldGroup = rowNode.group;
+    if (oldGroup === group) {
+        return;
     }
 
     rowNode.group = group;
     rowNode.updateHasChildren();
+
+    // Clear stale aggData and allChildrenCount when demoting from group to leaf.
+    // These must be cleared here because downstream stages (filterAggregatesStage)
+    // won't visit this node via changedPath since it's no longer a group.
+    if (oldGroup && !group) {
+        setAggData(rowNode, null, beans.colModel);
+        rowNode.setAllChildrenCount(null);
+    }
+
     beans.selectionSvc?.updateRowSelectable(rowNode);
     rowNode.dispatchRowEvent('groupChanged');
-}
+};
+
+export const setRowNodeGroup = (rowNode: RowNode, beans: BeanCollection, group: boolean): void => {
+    doSetRowNodeGroup(rowNode, beans, group);
+    doSetRowNodeGroup(rowNode.pinnedSibling, beans, group);
+};
 
-export function isRowGroupColLocked(column: AgColumn | undefined | null, beans: BeanCollection): boolean {
+export const isRowGroupColLocked = (column: AgColumn | undefined | null, beans: BeanCollection): boolean => {
     const { gos, rowGroupColsSvc } = beans;
 
     if (!rowGroupColsSvc || !column) {
@@ -67,17 +74,17 @@ export function isRowGroupColLocked(column: AgColumn | undefined | null, beans:
 
     const colIndex = rowGroupColsSvc.columns.findIndex((groupCol) => groupCol.getColId() === column.getColId());
     return groupLockGroupColumns > colIndex;
-}
+};
 
 /**
  * In AG-16700 the locale introduced a ${variable} and stopped concatenating the column name in the code
  * To avoid a breaking change we need to check if the variable is present and if not fallback to the old way of concatenating the column name.
  */
-export function getGroupingLocaleText(
+export const getGroupingLocaleText = (
     localeTextFunc: LocaleTextFunc,
     key: 'groupBy' | 'ungroupBy',
     displayName: string
-): string {
+): string => {
     const prefix = key === 'groupBy' ? 'Group by' : 'Un-Group by';
 
     const localStr = localeTextFunc(key, `${prefix} ${displayName}`, [displayName]);
@@ -88,4 +95,4 @@ export function getGroupingLocaleText(
     } else {
         return `${localStr} ${displayName}`;
     }
-}
+};
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts b/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts
index a598125044d..e48ad18776c 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts
@@ -4,6 +4,7 @@ import type {
     GroupCellRendererParams,
     IGroupCellRenderer,
     IGroupCellRendererCtrl,
+    IRowNode,
     RowNode,
     UserCompDetails,
 } from 'ag-grid-community';
@@ -187,7 +188,7 @@ export class GroupCellRendererCtrl extends BeanStub implements IGroupCellRendere
      */
     private setupExpand(): void {
         const { colModel } = this.beans;
-        const { eGridCell, column, suppressDoubleClickExpand } = this.params;
+        const { eGridCell, suppressDoubleClickExpand } = this.params;
 
         // Inserts the expand/collapse icons into the dom
         const addIconToDom = (iconName: 'groupExpanded' | 'groupContracted', element: HTMLElement) => {
@@ -244,9 +245,8 @@ export class GroupCellRendererCtrl extends BeanStub implements IGroupCellRendere
         };
 
         const setupListeners = () => {
-            // Cell double clicked
-            const isDoubleClickEdit = column?.isCellEditable(this.displayedNode) && this.gos.get('enableGroupEdit');
-            if (!isDoubleClickEdit && !suppressDoubleClickExpand) {
+            // Register dblclick for expand/collapse unless explicitly suppressed or the cell is group-editable
+            if (!suppressDoubleClickExpand && !this.isGroupCellEditable(this.displayedNode)) {
                 this.addManagedListeners(eGridCell, { dblclick: this.onCellDblClicked.bind(this) });
             }
             // Icons clicked
@@ -564,6 +564,16 @@ export class GroupCellRendererCtrl extends BeanStub implements IGroupCellRendere
         this.cbComp = this.destroyBean(this.cbComp);
     }
 
+    /** Whether the group cell is editable via groupRowEditable or enableGroupEdit. */
+    private isGroupCellEditable(node: RowNode | IRowNode): boolean {
+        const column = this.params.column;
+        return (
+            !!column &&
+            (!!column.getColDef().groupRowEditable || this.gos.get('enableGroupEdit')) &&
+            column.isCellEditable(node)
+        );
+    }
+
     /**
      * Called when the expand / contract icon is clicked.
      */
@@ -588,9 +598,7 @@ export class GroupCellRendererCtrl extends BeanStub implements IGroupCellRendere
             return;
         }
 
-        const cellEditable = this.params.column?.isCellEditable(this.params.node);
-
-        if (cellEditable) {
+        if (this.isGroupCellEditable(this.params.node)) {
             return;
         }
 
diff --git a/packages/ag-grid-enterprise/src/rowNumbers/rowNumbersService.ts b/packages/ag-grid-enterprise/src/rowNumbers/rowNumbersService.ts
index 8ae7d6a0df3..6db936cfc04 100644
--- a/packages/ag-grid-enterprise/src/rowNumbers/rowNumbersService.ts
+++ b/packages/ag-grid-enterprise/src/rowNumbers/rowNumbersService.ts
@@ -339,7 +339,7 @@ export class RowNumbersService extends BeanStub implements NamedBean, IRowNumber
         }
 
         e.preventDefault();
-        this.handleFocusAllCellsFromHeader();
+        this.selectAllCellsFromHeader();
     }
 
     private onHeaderClick(_e: MouseEvent): void {
@@ -350,11 +350,15 @@ export class RowNumbersService extends BeanStub implements NamedBean, IRowNumber
         ) {
             return;
         }
-        this.handleFocusAllCellsFromHeader();
+        this.focusAllCellsFromHeaderClick();
     }
 
-    private handleFocusAllCellsFromHeader(): void {
+    private selectAllCellsFromHeader(): void {
         _selectAllCells(this.beans);
+    }
+
+    private focusAllCellsFromHeaderClick(): void {
+        this.selectAllCellsFromHeader();
         this.focusFirstRenderedCellAtRowPosition();
     }
 
diff --git a/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts b/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts
index 10dc4b8206c..5683d4ca0c5 100644
--- a/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts
+++ b/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts
@@ -323,6 +323,7 @@ class AgSideBar extends Component implements ISideBar, FocusableContainer {
         let wrapper: ToolPanelWrapper;
         if (existingToolPanelWrapper) {
             wrapper = existingToolPanelWrapper;
+            wrapper.setDefParent(def.parent ?? null);
         } else {
             wrapper = this.createBean(new ToolPanelWrapper());
 
diff --git a/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts b/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts
index c8c5d06d8b0..7e3036441f8 100644
--- a/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts
+++ b/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts
@@ -68,6 +68,10 @@ export class ToolPanelWrapper extends Component {
         return this.defParent;
     }
 
+    public setDefParent(parent: HTMLElement | null): void {
+        this.defParent = parent;
+    }
+
     public setToolPanelDef(toolPanelDef: ToolPanelDef, params: IToolPanelParams): boolean {
         const { id, minWidth, maxWidth, width, parent } = toolPanelDef;
 
diff --git a/packages/ag-grid-enterprise/src/version.ts b/packages/ag-grid-enterprise/src/version.ts
index 320ce849941..ff797fa4963 100644
--- a/packages/ag-grid-enterprise/src/version.ts
+++ b/packages/ag-grid-enterprise/src/version.ts
@@ -1,2 +1,2 @@
 // DO NOT UPDATE MANUALLY: Generated from script during build time
-export const VERSION = '35.1.0-beta.20260320.1044';
+export const VERSION = '35.2.0-beta.20260324.1227';
diff --git a/packages/ag-grid-react/package.json b/packages/ag-grid-react/package.json
index b14ef209298..c641fb3ece3 100644
--- a/packages/ag-grid-react/package.json
+++ b/packages/ag-grid-react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-react",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "AG Grid React Component",
   "main": "./dist/package/index.cjs.js",
   "types": "./dist/types/src/index.d.ts",
@@ -31,7 +31,7 @@
   "devDependencies": {
     "@babel/runtime": "^7.27.1",
     "prop-types": "^15.6.2",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "@babel/plugin-proposal-throw-expressions": "^7.27.1",
     "@babel/preset-typescript": "^7.27.1",
     "@types/react": "~18.3.26",
@@ -44,7 +44,7 @@
   },
   "dependencies": {
     "prop-types": "^15.8.1",
-    "ag-grid-community": "35.1.0-beta.20260320.1044"
+    "ag-grid-community": "35.2.0-beta.20260324.1227"
   },
   "peerDependencies": {
     "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
diff --git a/packages/ag-grid-vue3/package.json b/packages/ag-grid-vue3/package.json
index 07a09a40669..bd598e955ef 100644
--- a/packages/ag-grid-vue3/package.json
+++ b/packages/ag-grid-vue3/package.json
@@ -1,7 +1,7 @@
 {
   "name": "ag-grid-vue3",
   "description": "AG Grid Vue 3 Component",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "author": "Sean Landsman <sean@thelandsmans.com>",
   "license": "MIT",
   "files": [
@@ -44,7 +44,7 @@
     "build-only:watch": "vite build --watch"
   },
   "dependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044"
+    "ag-grid-community": "35.2.0-beta.20260324.1227"
   },
   "devDependencies": {
     "vue": "^3.5.0",
diff --git a/packages/ag-grid-vue3/src/components/utils.ts b/packages/ag-grid-vue3/src/components/utils.ts
index 8733e6bcdcd..9855ef3fc57 100644
--- a/packages/ag-grid-vue3/src/components/utils.ts
+++ b/packages/ag-grid-vue3/src/components/utils.ts
@@ -1093,10 +1093,11 @@ export interface Props<TData> {
          * @agModule `RowDragModule`
          */
     rowDragManaged?: boolean,
-    /** When `true`, managed row dragging updates grouped column values so rows can move between groups. When `false`,
-         * managed dragging only reorders rows inside their existing group.
+    /** When `true`, the grid re-evaluates the grouping hierarchy after editing a grouped column value,
+         * moving the row to the correct group instantly. Also enables managed row dragging to update
+         * grouped column values so rows can move between groups.
          * @default false
-         * @agModule `RowDragModule`
+         * @agModule `RowGroupingModule` / `TreeDataModule`
          */
     refreshAfterGroupEdit?: boolean,
     /** Used if rowDragManaged is enabled and treeData is enabled,
diff --git a/plugins/ag-grid-generate-code-reference-files/package.json b/plugins/ag-grid-generate-code-reference-files/package.json
index 9d601c83223..512e0a5ecec 100644
--- a/plugins/ag-grid-generate-code-reference-files/package.json
+++ b/plugins/ag-grid-generate-code-reference-files/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-generate-code-reference-files",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "private": true,
   "dependencies": {
     "ag-shared": "0.0.1",
diff --git a/plugins/ag-grid-generate-example-files/package.json b/plugins/ag-grid-generate-example-files/package.json
index cc485c5e060..0ea84415538 100644
--- a/plugins/ag-grid-generate-example-files/package.json
+++ b/plugins/ag-grid-generate-example-files/package.json
@@ -1,10 +1,10 @@
 {
   "name": "ag-grid-generate-example-files",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "private": true,
   "dependencies": {
     "ag-shared": "0.0.1",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "glob": "8.0.3",
     "typescript": "~5.4.5",
     "cheerio": "^1.0.0",
diff --git a/plugins/ag-grid-task-autogen/package.json b/plugins/ag-grid-task-autogen/package.json
index 2b0449ee803..e8e04ba18d5 100644
--- a/plugins/ag-grid-task-autogen/package.json
+++ b/plugins/ag-grid-task-autogen/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-task-autogen",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "private": true,
   "dependencies": {
     "@nx/devkit": "20.3.1",
diff --git a/scripts/deployments/prep_and_archive/pushIfValidChangeset.sh b/scripts/deployments/prep_and_archive/pushIfValidChangeset.sh
index 1895a3ce452..9db5c6af0d3 100755
--- a/scripts/deployments/prep_and_archive/pushIfValidChangeset.sh
+++ b/scripts/deployments/prep_and_archive/pushIfValidChangeset.sh
@@ -8,9 +8,9 @@ fi
 
 RELEASE_VERSION=$1
 RELEASE_BRANCH=$2
-ALLOWED_FILES="package.json|yarn.lock|version.ts|licenseManager.ts|.env|README.md|baseUrl.ts|documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json"
+ALLOWED_FILES="package.json|yarn.lock|version.ts|licenseManager.ts|.env|README.md|baseUrl.ts|documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json|.mdoc"
 
-NON_PACKAGE_JSON_COUNT=`git status --porcelain | grep -Ev "$ALLOWED_FILES" | wc -l`
+NON_PACKAGE_JSON_COUNT=`git status --porcelain | grep -Ev "package.json|yarn.lock|version.ts|licenseManager.ts|.env|README.md|baseUrl.ts|documentation/ag-grid-docs/src/content/versions/ag-grid-versions.json|.mdoc" | wc -l`
 
 if [ $NON_PACKAGE_JSON_COUNT -ne 0 ];
 then
diff --git a/testing/accessibility/package.json b/testing/accessibility/package.json
index 228ab7e2da4..e72310fb1ec 100644
--- a/testing/accessibility/package.json
+++ b/testing/accessibility/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-accessibility",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "scripts": {
     "download-examples": "curl --retry 5 -retry-all-errors https://grid-staging.ag-grid.com/debug/all-examples.json > ./all-examples.json",
     "download-examples-local": "curl https://localhost:4610/debug/all-examples.json > ./all-examples.json",
@@ -18,11 +18,11 @@
     "@angular/platform-browser": "^19.0.0",
     "@angular/platform-browser-dynamic": "^19.0.0",
     "@angular/router": "^19.0.0",
-    "ag-grid-angular": "35.1.0-beta.20260320.1044",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
-    "ag-grid-enterprise": "35.1.0-beta.20260320.1044",
-    "ag-charts-community": "13.1.0-beta.20260320",
-    "ag-charts-enterprise": "13.1.0-beta.20260320",
+    "ag-grid-angular": "35.2.0-beta.20260324.1227",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
+    "ag-grid-enterprise": "35.2.0-beta.20260324.1227",
+    "ag-charts-community": "13.2.0-beta.20260324",
+    "ag-charts-enterprise": "13.2.0-beta.20260324",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"
diff --git a/testing/angular-tests/package.json b/testing/angular-tests/package.json
index 8b9d15f9a3c..58900db0dd7 100644
--- a/testing/angular-tests/package.json
+++ b/testing/angular-tests/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-angular-tests",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "private": true,
   "scripts": {
     "test:e2e": "jest --no-cache"
@@ -11,8 +11,8 @@
     "@angular/core": "^21.0.0",
     "@angular/platform-browser": "^21.0.0",
     "@angular/platform-browser-dynamic": "^21.0.0",
-    "ag-grid-angular": "35.1.0-beta.20260320.1044",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-angular": "35.2.0-beta.20260324.1227",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"
diff --git a/testing/behavioural/package.json b/testing/behavioural/package.json
index 2d140cc227d..8862a191391 100644
--- a/testing/behavioural/package.json
+++ b/testing/behavioural/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-behavioural-testing",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "private": true,
   "description": "Behavioural unit testing for ag-Grid",
   "dependencies": {
@@ -8,9 +8,9 @@
   },
   "type": "module",
   "devDependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
-    "ag-grid-enterprise": "35.1.0-beta.20260320.1044",
-    "ag-grid-react": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
+    "ag-grid-enterprise": "35.2.0-beta.20260324.1227",
+    "ag-grid-react": "35.2.0-beta.20260324.1227",
     "@types/react": "^18.3.23",
     "@types/react-dom": "^18.3.7",
     "@testing-library/dom": "^10.4.0",
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-advanced.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-advanced.test.ts
index 6ddc25aea8f..2d0eab8d235 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-advanced.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-advanced.test.ts
@@ -268,7 +268,7 @@ describe('distribution default handler', () => {
         expect(api.getRowNode('a2')?.data?.amount).toBe(888);
     });
 
-    test('built-in aggFunc not in record without default handler overwrites all', async () => {
+    test('built-in aggFunc not in record without default handler inherits default strategy', async () => {
         const api = await createSimpleGrid(
             'unmatched-sum-no-default',
             [
@@ -288,9 +288,9 @@ describe('distribution default handler', () => {
         group.setDataValue('amount', 42, 'ui');
         await asyncSetTimeout(0);
 
-        // sum is not in the record, no default handler → overwrites all children
-        expect(api.getRowNode('a1')?.data?.amount).toBe(42);
-        expect(api.getRowNode('a2')?.data?.amount).toBe(42);
+        // sum is not in the record, no default handler → inherits default (uniform for sum)
+        expect(api.getRowNode('a1')?.data?.amount).toBe(21);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(21);
     });
 
     test('custom function entry in record is invoked directly', async () => {
@@ -326,8 +326,8 @@ describe('distribution default handler', () => {
         expect(api.getRowNode('a2')?.data?.amount).toBe(777);
     });
 
-    test('unknown aggFunc without record or default handler defaults to overwrite', async () => {
-        const api = await gridsManager.createGridAndWait('default-overwrite', {
+    test('function aggFunc without options is disabled by default (no distribution)', async () => {
+        const api = await gridsManager.createGridAndWait('fn-agg-disabled', {
             defaultColDef: { cellEditor: 'agTextCellEditor' },
             groupDisplayType: 'custom',
             columnDefs: [
@@ -355,7 +355,41 @@ describe('distribution default handler', () => {
         group.setDataValue('amount', 42, 'ui');
         await asyncSetTimeout(0);
 
-        // Custom aggFunc (function, not string) → overwrites all
+        // Function aggFuncs are disabled by default — children unchanged
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+
+    test('function aggFunc with distribution: true enables overwrite', async () => {
+        const api = await gridsManager.createGridAndWait('fn-agg-enabled', {
+            defaultColDef: { cellEditor: 'agTextCellEditor' },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'region', rowGroup: true, hide: true },
+                { field: 'country', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: (params) => params.values.reduce((a: number, b: number) => a + b, 0),
+                    editable: true,
+                    groupRowEditable: true,
+                    groupRowValueSetter: { distribution: true },
+                },
+            ],
+            rowData: [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data?.id,
+        });
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 42, 'ui');
+        await asyncSetTimeout(0);
+
+        // distribution: true enables function aggFuncs with 'overwrite'
         expect(api.getRowNode('a1')?.data?.amount).toBe(42);
         expect(api.getRowNode('a2')?.data?.amount).toBe(42);
     });
@@ -510,7 +544,7 @@ describe('percentage zero total direct path', () => {
 // --- String aggFunc that is not sum/avg/first/last with default handler ---
 
 describe('string aggFunc edge cases', () => {
-    test('unknown string aggFunc without default handler overwrites all', async () => {
+    test('unknown string aggFunc without default handler is disabled (no children modified)', async () => {
         const api = await createSimpleGrid(
             'unknown-string-agg',
             [
@@ -529,8 +563,9 @@ describe('string aggFunc edge cases', () => {
         group.setDataValue('amount', 42, 'ui');
         await asyncSetTimeout(0);
 
-        expect(api.getRowNode('a1')?.data?.amount).toBe(42);
-        expect(api.getRowNode('a2')?.data?.amount).toBe(42);
+        // Custom aggFuncs are disabled by default — children are not modified
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
     test('unknown string aggFunc with default handler calls default', async () => {
@@ -721,17 +756,17 @@ describe.each(SETTER_MODES)('decimal precision via %s', (_label, makeSetter) =>
     });
 });
 
-// --- 'none' strategy ---
+// --- Suppressed distribution ---
 
-describe("'none' distribution strategy", () => {
-    test("'none' string suppresses distribution", async () => {
+describe('suppressed distribution strategy', () => {
+    test('false suppresses distribution', async () => {
         const api = await createSimpleGrid(
             'none-string',
             [
                 { id: 'a1', region: 'R', country: 'C', amount: 10 },
                 { id: 'a2', region: 'R', country: 'C', amount: 20 },
             ],
-            { groupRowValueSetter: (params) => distributeGroupValue(params, { distribution: 'none' }) }
+            { groupRowValueSetter: (params) => distributeGroupValue(params, { distribution: false }) }
         );
 
         const group = api.getRowNode('row-group-region-R-country-C')!;
@@ -779,14 +814,14 @@ describe("'none' distribution strategy", () => {
         expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
-    test("'none' in per-aggFunc record suppresses for that aggFunc only", async () => {
+    test('false in per-aggFunc record suppresses for that aggFunc only', async () => {
         const api = await createSimpleGrid(
             'none-record',
             [
                 { id: 'a1', region: 'R', country: 'C', amount: 10 },
                 { id: 'a2', region: 'R', country: 'C', amount: 20 },
             ],
-            { groupRowValueSetter: (params) => distributeGroupValue(params, { distribution: { sum: 'none' } }) }
+            { groupRowValueSetter: (params) => distributeGroupValue(params, { distribution: { sum: false } }) }
         );
 
         const group = api.getRowNode('row-group-region-R-country-C')!;
@@ -797,7 +832,7 @@ describe("'none' distribution strategy", () => {
         expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
-    test('false in per-aggFunc record suppresses for that aggFunc only', async () => {
+    test('null in per-aggFunc record suppresses for that aggFunc only', async () => {
         const api = await createSimpleGrid(
             'none-false-record',
             [
@@ -815,20 +850,284 @@ describe("'none' distribution strategy", () => {
         expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
-    test("'none' on colDef options object suppresses distribution", async () => {
+    test('false on colDef options object suppresses distribution', async () => {
         const api = await createSimpleGrid(
             'none-coldef',
             [
                 { id: 'a1', region: 'R', country: 'C', amount: 10 },
                 { id: 'a2', region: 'R', country: 'C', amount: 20 },
             ],
-            { groupRowValueSetter: { distribution: 'none' } }
+            { groupRowValueSetter: { distribution: false } }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 100, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+});
+
+// --- undefined inherits from parent ---
+
+describe('undefined in record/options inherits from parent defaults', () => {
+    test('undefined entry in per-aggFunc record inherits default for sum (uniform)', async () => {
+        const api = await createSimpleGrid(
+            'inherit-undefined-record',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: { sum: undefined, avg: 'percentage' },
+                    }),
+            }
+        );
+
+        // sum: undefined → falls through to default (uniform for sum)
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 60, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')?.data?.amount).toBe(30);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(30);
+    });
+
+    test('undefined distribution in options object entry inherits default for sum', async () => {
+        const api = await createSimpleGrid(
+            'inherit-undefined-opts-entry',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 10 },
+                { id: 'a3', region: 'R', country: 'C', amount: 10 },
+            ],
+            {
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: { sum: { distribution: undefined, precision: 0 } },
+                    }),
+            }
+        );
+
+        // distribution: undefined inside options object → inherits default for sum (uniform)
+        // precision: 0 still applies
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 10, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')?.data?.amount).toBe(4);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(3);
+        expect(api.getRowNode('a3')?.data?.amount).toBe(3);
+    });
+
+    test('undefined top-level distribution inherits default for each aggFunc', async () => {
+        const api = await createSimpleGrid(
+            'inherit-undefined-top',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                groupRowValueSetter: (params) => distributeGroupValue(params, { distribution: undefined }),
+            }
+        );
+
+        // undefined top-level distribution → default for sum (uniform)
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 60, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')?.data?.amount).toBe(30);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(30);
+    });
+
+    test('undefined in record for count/min/max inherits disabled-by-default (no distribution)', async () => {
+        const api = await createSimpleGrid(
+            'inherit-undefined-count',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'count',
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: { count: undefined },
+                    }),
+            }
+        );
+
+        // count: undefined → inherits disabled-by-default (suppressed)
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 100, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+
+    test('explicit strategy in record overrides disabled-by-default for count', async () => {
+        const api = await createSimpleGrid(
+            'override-count-overwrite',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'count',
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: { count: 'overwrite' },
+                    }),
+            }
         );
 
+        // count: 'overwrite' → explicitly enabled
         const group = api.getRowNode('row-group-region-R-country-C')!;
         group.setDataValue('amount', 100, 'ui');
         await asyncSetTimeout(0);
 
+        expect(api.getRowNode('a1')?.data?.amount).toBe(100);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(100);
+    });
+});
+
+// --- Non-distributable aggFuncs with custom functions ---
+
+describe('non-distributable aggFuncs with custom functions', () => {
+    test('per-aggFunc record with function for count invokes the function', async () => {
+        const calls: number[] = [];
+        const api = await createSimpleGrid(
+            'count-custom-fn',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'count',
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: {
+                            count: (p) => {
+                                calls.push(Number(p.newValue));
+                                for (const child of p.aggregatedChildren) {
+                                    child.setDataValue(p.column, 777, 'data');
+                                }
+                                return true;
+                            },
+                        },
+                    }),
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 50, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(calls).toEqual([50]);
+        expect(api.getRowNode('a1')?.data?.amount).toBe(777);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(777);
+    });
+
+    test('default handler is NOT invoked for non-distributable aggFuncs not in record', async () => {
+        const defaultCalls: unknown[] = [];
+        const api = await createSimpleGrid(
+            'count-default-handler',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'count',
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: { sum: 'percentage' },
+                        default: (p) => {
+                            defaultCalls.push(p.newValue);
+                            for (const child of p.aggregatedChildren) {
+                                child.setDataValue(p.column, 888, 'data');
+                            }
+                        },
+                    }),
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 50, 'ui');
+        await asyncSetTimeout(0);
+
+        // count is non-distributable and not listed in the record → disabled, default handler NOT called
+        expect(defaultCalls).toHaveLength(0);
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+
+    test('groupRowValueSetter as custom function with count aggFunc is always invoked', async () => {
+        const calls: number[] = [];
+        const api = await createSimpleGrid(
+            'count-setter-fn',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'count',
+                groupRowValueSetter: (params) => {
+                    calls.push(Number(params.newValue));
+                    for (const child of params.aggregatedChildren) {
+                        child.setDataValue(params.column, 555, 'data');
+                    }
+                    return true;
+                },
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 50, 'ui');
+        await asyncSetTimeout(0);
+
+        // Function setter bypasses all distribution checks
+        expect(calls).toEqual([50]);
+        expect(api.getRowNode('a1')?.data?.amount).toBe(555);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(555);
+    });
+
+    test('per-aggFunc record with options object containing custom getValue/setValue for min', async () => {
+        const api = await createSimpleGrid(
+            'min-custom-io',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10, target: 0 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20, target: 0 },
+            ],
+            {
+                aggFunc: 'min',
+                groupRowValueSetter: (params) =>
+                    distributeGroupValue(params, {
+                        distribution: {
+                            min: {
+                                distribution: 'overwrite',
+                                setValue: ({ node, value }) => {
+                                    const old = node.data?.target;
+                                    node.data.target = value;
+                                    return old !== value;
+                                },
+                            },
+                        },
+                    }),
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 99, 'ui');
+        await asyncSetTimeout(0);
+
+        // Custom setValue writes to 'target' field, not 'amount'
+        expect(api.getRowNode('a1')?.data?.target).toBe(99);
+        expect(api.getRowNode('a2')?.data?.target).toBe(99);
+        // 'amount' field unchanged
         expect(api.getRowNode('a1')?.data?.amount).toBe(10);
         expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
@@ -1068,7 +1367,7 @@ describe('avg with percentage via direct path', () => {
 // --- Record edge cases ---
 
 describe('distribution record edge cases', () => {
-    test('record with missing aggFunc key and no default uses built-in overwrite', async () => {
+    test('record with missing aggFunc key for count inherits disabled-by-default', async () => {
         const api = await createSimpleGrid(
             'record-missing-key',
             [
@@ -1084,13 +1383,13 @@ describe('distribution record edge cases', () => {
             }
         );
 
-        // count is not in record, no default → falls back to built-in writeAll
+        // count is not in record, no default → disabled by default (no distribution)
         const group = api.getRowNode('row-group-region-R-country-C')!;
         group.setDataValue('amount', 42, 'ui');
         await asyncSetTimeout(0);
 
-        expect(api.getRowNode('a1')?.data?.amount).toBe(42);
-        expect(api.getRowNode('a2')?.data?.amount).toBe(42);
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
     test('record with custom function returning false propagates false', async () => {
@@ -1231,7 +1530,7 @@ describe('options object directly on colDef', () => {
         expect(api.getRowNode('a2')?.data?.amount).toBe(160);
     });
 
-    test('first aggFunc short-circuits even with distribution options present', async () => {
+    test('first aggFunc with top-level distribution options is suppressed', async () => {
         const api = await createSimpleGrid(
             'coldef-first-with-opts',
             [
@@ -1245,12 +1544,12 @@ describe('options object directly on colDef', () => {
         group.setDataValue('amount', 99, 'ui');
         await asyncSetTimeout(0);
 
-        // first always sets only the first child, distribution options are ignored
-        expect(api.getRowNode('a1')?.data?.amount).toBe(99);
+        // first is non-distributable — top-level distribution doesn't enable it
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
         expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
-    test('last aggFunc short-circuits even with distribution options present', async () => {
+    test('last aggFunc with top-level distribution options is suppressed', async () => {
         const api = await createSimpleGrid(
             'coldef-last-with-opts',
             [
@@ -1264,17 +1563,36 @@ describe('options object directly on colDef', () => {
         group.setDataValue('amount', 99, 'ui');
         await asyncSetTimeout(0);
 
-        // last always sets only the last child, distribution options are ignored
+        // last is non-distributable — top-level distribution doesn't enable it
         expect(api.getRowNode('a1')?.data?.amount).toBe(10);
-        expect(api.getRowNode('a2')?.data?.amount).toBe(99);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
-    test('record with function entry on colDef options object', async () => {
-        const customCalls: GroupRowValueSetterParams[] = [];
+    test('first aggFunc with per-aggFunc record entry uses overwrite', async () => {
+        const api = await createSimpleGrid(
+            'coldef-first-record',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            { aggFunc: 'first', groupRowValueSetter: { distribution: { first: true } } }
+        );
 
-        const api = await gridsManager.createGridAndWait('coldef-record-fn', {
-            defaultColDef: { cellEditor: 'agTextCellEditor' },
-            groupDisplayType: 'custom',
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 99, 'ui');
+        await asyncSetTimeout(0);
+
+        // per-aggFunc entry with true → overwrite all children
+        expect(api.getRowNode('a1')?.data?.amount).toBe(99);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(99);
+    });
+
+    test('record with function entry on colDef options object', async () => {
+        const customCalls: GroupRowValueSetterParams[] = [];
+
+        const api = await gridsManager.createGridAndWait('coldef-record-fn', {
+            defaultColDef: { cellEditor: 'agTextCellEditor' },
+            groupDisplayType: 'custom',
             columnDefs: [
                 { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
                 { field: 'region', rowGroup: true, hide: true },
@@ -1353,14 +1671,14 @@ describe('options object directly on colDef', () => {
         group.setDataValue('amount', 100, 'ui');
         await asyncSetTimeout(0);
 
-        // Custom aggFunc (function) → aggFunc resolves to null → not in record → default handler
+        // Function aggFunc → not a string → not in record → default handler
         expect(defaultCalls).toHaveLength(1);
         expect(api.getRowNode('a1')?.data?.amount).toBe(888);
         expect(api.getRowNode('a2')?.data?.amount).toBe(888);
     });
 
-    test('custom (non-string) aggFunc with no default overwrites all', async () => {
-        const api = await gridsManager.createGridAndWait('coldef-custom-agg-no-default', {
+    test('function aggFunc with record and no default is disabled (no distribution)', async () => {
+        const api = await gridsManager.createGridAndWait('coldef-fn-agg-no-default', {
             defaultColDef: { cellEditor: 'agTextCellEditor' },
             groupDisplayType: 'custom',
             columnDefs: [
@@ -1388,9 +1706,9 @@ describe('options object directly on colDef', () => {
         group.setDataValue('amount', 42, 'ui');
         await asyncSetTimeout(0);
 
-        // Custom aggFunc, not in record, no default → writeAll
-        expect(api.getRowNode('a1')?.data?.amount).toBe(42);
-        expect(api.getRowNode('a2')?.data?.amount).toBe(42);
+        // Function aggFunc, not in record, no default → disabled by default
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
     });
 
     test('consecutive edits with options object on colDef', async () => {
@@ -1421,3 +1739,421 @@ describe('options object directly on colDef', () => {
         expect(group.aggData?.amount).toBe(100);
     });
 });
+
+describe('precision rounds child values, not the re-aggregated group value', () => {
+    test('sum: precision 0 produces integer leaf values and integer group sum', async () => {
+        const api = await createSimpleGrid(
+            'precision-sum-int',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+                { id: 'a3', region: 'R', country: 'C', amount: 30 },
+            ],
+            { groupRowValueSetter: { distribution: 'uniform', precision: 0 } }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 100, 'ui');
+        await asyncSetTimeout(0);
+
+        // Leaf values are rounded to integers with remainder spread: [34, 33, 33]
+        const v1 = api.getRowNode('a1')!.data!.amount;
+        const v2 = api.getRowNode('a2')!.data!.amount;
+        const v3 = api.getRowNode('a3')!.data!.amount;
+        expect(Number.isInteger(v1)).toBe(true);
+        expect(Number.isInteger(v2)).toBe(true);
+        expect(Number.isInteger(v3)).toBe(true);
+        expect(v1 + v2 + v3).toBe(100);
+
+        // The sum of integers is always an integer — group value honours precision
+        expect(group.aggData?.amount).toBe(100);
+        expect(Number.isInteger(group.aggData?.amount)).toBe(true);
+    });
+
+    test('avg: precision 0 with uniform — leaf values are integers, group avg honours precision', async () => {
+        // Uniform avg sets all children to the target value → avg matches exactly.
+        const api = await createSimpleGrid(
+            'precision-avg-uniform',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+                { id: 'a3', region: 'R', country: 'C', amount: 30 },
+            ],
+            { aggFunc: 'avg', groupRowValueSetter: { distribution: 'uniform', precision: 0 } }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+
+        // Edit group avg to 10 — uniform writes 10 to each child → avg(10,10,10) = 10 ✓
+        group.setDataValue('amount', 10, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')!.data!.amount).toBe(10);
+        expect(api.getRowNode('a2')!.data!.amount).toBe(10);
+        expect(api.getRowNode('a3')!.data!.amount).toBe(10);
+        expect(group.aggData?.amount).toMatchObject({ value: 10 });
+    });
+
+    test('avg: precision 0 with percentage — leaf values are integers, sum is exact', async () => {
+        const api = await createSimpleGrid(
+            'precision-avg-pct',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+                { id: 'a3', region: 'R', country: 'C', amount: 30 },
+            ],
+            { aggFunc: 'avg', groupRowValueSetter: { distribution: 'percentage', precision: 0 } }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        // Old avg = 20, edit to 25
+        // Target sum = 75, old sum = 60 — distributes proportionally then rounds
+        group.setDataValue('amount', 25, 'ui');
+        await asyncSetTimeout(0);
+
+        const w1 = api.getRowNode('a1')!.data!.amount;
+        const w2 = api.getRowNode('a2')!.data!.amount;
+        const w3 = api.getRowNode('a3')!.data!.amount;
+
+        // All leaf values are integers (precision: 0)
+        expect(Number.isInteger(w1)).toBe(true);
+        expect(Number.isInteger(w2)).toBe(true);
+        expect(Number.isInteger(w3)).toBe(true);
+
+        // The sum is exact in integer space (remainder distributed)
+        expect(w1 + w2 + w3).toBe(75);
+        expect(group.aggData?.amount).toMatchObject({ value: 25 });
+    });
+
+    test('avg: precision 0 with increment — leaf values are integers, group avg matches', async () => {
+        const api = await createSimpleGrid(
+            'precision-avg-inc',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 10 },
+                { id: 'a3', region: 'R', country: 'C', amount: 10 },
+                { id: 'a4', region: 'R', country: 'C', amount: 10 },
+            ],
+            { aggFunc: 'avg', groupRowValueSetter: { distribution: 'increment', precision: 0 } }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        expect(group.aggData?.amount).toMatchObject({ value: 10 });
+
+        // Edit avg from 10 to 11 → for avg+increment, full delta added to every child
+        group.setDataValue('amount', 11, 'ui');
+        await asyncSetTimeout(0);
+
+        expect(api.getRowNode('a1')!.data!.amount).toBe(11);
+        expect(api.getRowNode('a2')!.data!.amount).toBe(11);
+        expect(api.getRowNode('a3')!.data!.amount).toBe(11);
+        expect(api.getRowNode('a4')!.data!.amount).toBe(11);
+        expect(group.aggData?.amount).toMatchObject({ value: 11 });
+    });
+
+    test('sum: precision 0 with uniform on 7 children — leaf values are integers, group sum is exact', async () => {
+        // 100 / 7 = 14.285... → with precision 0: remainder-spread integers summing to 100
+        const api = await createSimpleGrid(
+            'precision-sum-7kids',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 1 },
+                { id: 'a2', region: 'R', country: 'C', amount: 1 },
+                { id: 'a3', region: 'R', country: 'C', amount: 1 },
+                { id: 'a4', region: 'R', country: 'C', amount: 1 },
+                { id: 'a5', region: 'R', country: 'C', amount: 1 },
+                { id: 'a6', region: 'R', country: 'C', amount: 1 },
+                { id: 'a7', region: 'R', country: 'C', amount: 1 },
+            ],
+            { groupRowValueSetter: { distribution: 'uniform', precision: 0 } }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 100, 'ui');
+        await asyncSetTimeout(0);
+
+        const values = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7'].map((id) => api.getRowNode(id)!.data!.amount);
+
+        // All leaf values are integers
+        for (const v of values) {
+            expect(Number.isInteger(v)).toBe(true);
+        }
+        // Sum is exact — for sum, the sum of integers is always an integer
+        expect(values.reduce((a, b) => a + b, 0)).toBe(100);
+        expect(group.aggData?.amount).toBe(100);
+    });
+
+    test('avg column shows non-integer aggData when all leaf values are integers', async () => {
+        // Demonstrates the precision note: precision rounds child values, not the re-aggregated
+        // group value. A sum column distributes 10 across 3 children with precision:0 → [4, 3, 3].
+        // A second avg column on the same field re-aggregates those integers: avg(4, 3, 3) ≈ 3.33,
+        // which is NOT an integer despite all leaf values being integers.
+        const api = await createSimpleGrid(
+            'precision-avg-non-int',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 1 },
+                { id: 'a2', region: 'R', country: 'C', amount: 1 },
+                { id: 'a3', region: 'R', country: 'C', amount: 1 },
+            ],
+            { groupRowValueSetter: { distribution: 'uniform', precision: 0 } },
+            [{ colId: 'amount_avg', field: 'amount', aggFunc: 'avg' }]
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        // Distribute 10 across 3 children with precision:0 → [4, 3, 3]
+        group.setDataValue('amount', 10, 'ui');
+        await asyncSetTimeout(0);
+
+        // All leaf values are integers
+        const v1 = api.getRowNode('a1')!.data!.amount;
+        const v2 = api.getRowNode('a2')!.data!.amount;
+        const v3 = api.getRowNode('a3')!.data!.amount;
+        expect(Number.isInteger(v1)).toBe(true);
+        expect(Number.isInteger(v2)).toBe(true);
+        expect(Number.isInteger(v3)).toBe(true);
+        expect(v1 + v2 + v3).toBe(10);
+
+        // The sum column's aggData is an integer (sum of integers is always an integer)
+        expect(group.aggData?.amount).toBe(10);
+        expect(Number.isInteger(group.aggData?.amount)).toBe(true);
+
+        // The avg column's aggData is NOT an integer: avg(4, 3, 3) = 10/3 ≈ 3.333...
+        const avgAgg = group.aggData?.amount_avg;
+        expect(avgAgg).toMatchObject({ value: expect.closeTo(10 / 3, 10) });
+        expect(Number.isInteger(avgAgg.value)).toBe(false);
+    });
+});
+
+// --- Regression: record-mode with default:false suppresses columns without aggFunc ---
+
+describe('record-mode default suppression', () => {
+    test('record distribution with default:false suppresses column without aggFunc', async () => {
+        const api = await createSimpleGrid(
+            'record-default-false-no-agg',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: undefined,
+                groupRowValueSetter: (params: GroupRowValueSetterParams) =>
+                    distributeGroupValue(params, { distribution: { sum: 'percentage' }, default: false }),
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 99, 'ui');
+        await asyncSetTimeout(0);
+
+        // default:false suppresses distribution — children unchanged
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+
+    test('record distribution with default:null suppresses column without aggFunc', async () => {
+        const api = await createSimpleGrid(
+            'record-default-null-no-agg',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: undefined,
+                groupRowValueSetter: (params: GroupRowValueSetterParams) =>
+                    distributeGroupValue(params, { distribution: { sum: 'percentage' }, default: null }),
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 99, 'ui');
+        await asyncSetTimeout(0);
+
+        // default:null suppresses distribution — children unchanged
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+
+    test('record distribution without default falls through to overwrite for no-aggFunc column', async () => {
+        const api = await createSimpleGrid(
+            'record-no-default-no-agg',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: undefined,
+                groupRowValueSetter: (params: GroupRowValueSetterParams) =>
+                    distributeGroupValue(params, { distribution: { sum: 'percentage' } }),
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 99, 'ui');
+        await asyncSetTimeout(0);
+
+        // No default, no aggFunc → falls through to 'overwrite'
+        expect(api.getRowNode('a1')?.data?.amount).toBe(99);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(99);
+    });
+});
+
+// --- Tests for `true` distribution entry ---
+
+describe('distribution: true', () => {
+    test('true enables custom aggFunc with overwrite (via options object)', async () => {
+        const api = await createSimpleGrid(
+            'true-custom-agg-options',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'myCustomAgg',
+                groupRowValueSetter: { distribution: true },
+            },
+            undefined,
+            { aggFuncs: CUSTOM_AGG_FUNCS }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 42, 'ui');
+        await asyncSetTimeout(0);
+
+        // true enables normally-disabled custom aggFuncs with 'overwrite'
+        expect(api.getRowNode('a1')?.data?.amount).toBe(42);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(42);
+    });
+
+    test('distribution: true does NOT enable count aggFunc (same as default)', async () => {
+        const api = await createSimpleGrid(
+            'true-count-agg',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'count',
+                groupRowValueSetter: { distribution: true },
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 99, 'ui');
+        await asyncSetTimeout(0);
+
+        // true at top level does NOT enable non-distributable aggFuncs — children unchanged
+        expect(api.getRowNode('a1')?.data?.amount).toBe(10);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(20);
+    });
+
+    test('true in per-aggFunc record enables specific aggFunc', async () => {
+        const api = await createSimpleGrid(
+            'true-record-entry',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'myCustomAgg',
+                groupRowValueSetter: { distribution: { myCustomAgg: true } },
+            },
+            undefined,
+            { aggFuncs: CUSTOM_AGG_FUNCS }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 50, 'ui');
+        await asyncSetTimeout(0);
+
+        // true as a record entry enables with 'overwrite'
+        expect(api.getRowNode('a1')?.data?.amount).toBe(50);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(50);
+    });
+
+    test('true preserves built-in default for sum (uniform)', async () => {
+        const api = await createSimpleGrid(
+            'true-sum-default',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'sum',
+                groupRowValueSetter: { distribution: true },
+            }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 60, 'ui');
+        await asyncSetTimeout(0);
+
+        // true for sum still uses 'uniform' (built-in default)
+        expect(api.getRowNode('a1')?.data?.amount).toBe(30);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(30);
+        expect(group.aggData?.amount).toBe(60);
+    });
+
+    test('true as default fallback enables custom aggFuncs', async () => {
+        const api = await createSimpleGrid(
+            'true-default-fallback',
+            [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            {
+                aggFunc: 'myCustomAgg',
+                groupRowValueSetter: (params: GroupRowValueSetterParams) =>
+                    distributeGroupValue(params, { default: true }),
+            },
+            undefined,
+            { aggFuncs: CUSTOM_AGG_FUNCS }
+        );
+
+        const group = api.getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 42, 'ui');
+        await asyncSetTimeout(0);
+
+        // true as default fallback enables custom aggFuncs with 'overwrite'
+        expect(api.getRowNode('a1')?.data?.amount).toBe(42);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(42);
+    });
+
+    test('true in record overrides false from deep-merged defaultColDef', async () => {
+        const api = gridsManager.createGridAndWait('true-override-false', {
+            defaultColDef: {
+                cellEditor: 'agTextCellEditor',
+                editable: true,
+                groupRowEditable: true,
+                groupRowValueSetter: { distribution: { count: false } },
+            },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'region', rowGroup: true, hide: true },
+                { field: 'country', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'count',
+                    // Column-level true overrides defaultColDef's false after deep merge
+                    groupRowValueSetter: { distribution: { count: true } },
+                },
+            ],
+            rowData: [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params: any) => params.data?.id,
+        });
+
+        await asyncSetTimeout(0);
+
+        const group = (await api).getRowNode('row-group-region-R-country-C')!;
+        group.setDataValue('amount', 77, 'ui');
+        await asyncSetTimeout(0);
+
+        // true overrides false, enabling count with 'overwrite'
+        expect((await api).getRowNode('a1')?.data?.amount).toBe(77);
+        expect((await api).getRowNode('a2')?.data?.amount).toBe(77);
+    });
+});
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts
index 56fa07ebf04..6560275f485 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts
@@ -47,75 +47,29 @@ describe.each(EDIT_MODES)('distributeGroupValue aggFunc strategies (%s)', (editM
         `);
     });
 
-    test('min: writes to the child holding the minimum value', async () => {
+    test('min: suppressed by default — no children are modified', async () => {
         const api = await createGrid('distribute-min', { aggFunc: 'min', groupRowValueSetter: distributeGroupValue });
 
         const usaNode = api.getRowNode('row-group-region-Americas-country-USA')!;
-        // USA children: us-nyc=70, us-la=30. Min holder is us-la (30).
+        // min is disabled by default — edit has no effect
         await performEdit(editMode, api, usaNode, 'amount', 10);
 
-        // Only the min holder (us-la) is updated; us-nyc stays at 70
         expect(api.getRowNode('us-nyc')?.data?.amount).toBe(70);
-        expect(api.getRowNode('us-la')?.data?.amount).toBe(10);
-        expect(usaNode.aggData?.amount).toBe(10);
-
-        await new GridRows(api, 'after min edit').check(`
-            ROOT id:ROOT_NODE_ID
-            ├─┬ filler id:row-group-region-Europe amount:30
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:30
-            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:30
-            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:30
-            │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
-            │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
-            │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:30
-            │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
-            │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
-            └─┬ filler id:row-group-region-Americas amount:10
-            · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:10
-            · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
-            · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:10
-            · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:25
-            · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
-            · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
-        `);
+        expect(api.getRowNode('us-la')?.data?.amount).toBe(30);
     });
 
-    test('max: writes to the child holding the maximum value', async () => {
+    test('max: suppressed by default — no children are modified', async () => {
         const api = await createGrid('distribute-max', { aggFunc: 'max', groupRowValueSetter: distributeGroupValue });
 
         const usaNode = api.getRowNode('row-group-region-Americas-country-USA')!;
-        // USA children: us-nyc=70, us-la=30. Max holder is us-nyc (70).
+        // max is disabled by default — edit has no effect
         await performEdit(editMode, api, usaNode, 'amount', 99);
 
-        // Only the max holder (us-nyc) is updated; us-la stays at 30
-        expect(api.getRowNode('us-nyc')?.data?.amount).toBe(99);
+        expect(api.getRowNode('us-nyc')?.data?.amount).toBe(70);
         expect(api.getRowNode('us-la')?.data?.amount).toBe(30);
-        expect(usaNode.aggData?.amount).toBe(99);
-
-        await new GridRows(api, 'after max edit').check(`
-            ROOT id:ROOT_NODE_ID
-            ├─┬ filler id:row-group-region-Europe amount:30
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:30
-            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:30
-            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:30
-            │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
-            │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
-            │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:30
-            │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
-            │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
-            └─┬ filler id:row-group-region-Americas amount:99
-            · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:99
-            · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:99
-            · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
-            · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:35
-            · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
-            · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
-        `);
     });
 
-    test('first: only sets the first child', async () => {
+    test('first: suppressed by default — no children are modified', async () => {
         const api = await createGrid('distribute-first', {
             aggFunc: 'first',
             groupRowValueSetter: distributeGroupValue,
@@ -124,32 +78,12 @@ describe.each(EDIT_MODES)('distributeGroupValue aggFunc strategies (%s)', (editM
         const franceNode = api.getRowNode('row-group-region-Europe-country-France')!;
         await performEdit(editMode, api, franceNode, 'amount', 999);
 
-        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(999);
+        // first is disabled by default — children unchanged
+        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(30);
         expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(30);
-
-        await new GridRows(api, 'after first edit').check(`
-            ROOT id:ROOT_NODE_ID
-            ├─┬ filler id:row-group-region-Europe amount:999
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:999
-            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:999
-            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:30
-            │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
-            │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
-            │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:30
-            │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
-            │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
-            └─┬ filler id:row-group-region-Americas amount:70
-            · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:70
-            · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
-            · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
-            · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:35
-            · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
-            · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
-        `);
     });
 
-    test('last: only sets the last child', async () => {
+    test('last: suppressed by default — no children are modified', async () => {
         const api = await createGrid('distribute-last', {
             aggFunc: 'last',
             groupRowValueSetter: distributeGroupValue,
@@ -158,32 +92,38 @@ describe.each(EDIT_MODES)('distributeGroupValue aggFunc strategies (%s)', (editM
         const franceNode = api.getRowNode('row-group-region-Europe-country-France')!;
         await performEdit(editMode, api, franceNode, 'amount', 999);
 
+        // last is disabled by default — children unchanged
         expect(api.getRowNode('fr-paris')?.data?.amount).toBe(30);
+        expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(30);
+    });
+
+    test('first with explicit per-aggFunc overwrite: writes to all children', async () => {
+        const api = await createGrid('distribute-first-overwrite', {
+            aggFunc: 'first',
+            groupRowValueSetter: { distribution: { first: 'overwrite' } },
+        });
+
+        const franceNode = api.getRowNode('row-group-region-Europe-country-France')!;
+        await performEdit(editMode, api, franceNode, 'amount', 999);
+
+        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(999);
         expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(999);
+    });
 
-        await new GridRows(api, 'after last edit').check(`
-            ROOT id:ROOT_NODE_ID
-            ├─┬ filler id:row-group-region-Europe amount:30
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:999
-            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:30
-            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:999
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:30
-            │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
-            │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
-            │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:30
-            │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
-            │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
-            └─┬ filler id:row-group-region-Americas amount:25
-            · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:30
-            · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
-            · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
-            · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:25
-            · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
-            · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
-        `);
+    test('last with explicit per-aggFunc true: writes to all children', async () => {
+        const api = await createGrid('distribute-last-true', {
+            aggFunc: 'last',
+            groupRowValueSetter: { distribution: { last: true } },
+        });
+
+        const franceNode = api.getRowNode('row-group-region-Europe-country-France')!;
+        await performEdit(editMode, api, franceNode, 'amount', 999);
+
+        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(999);
+        expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(999);
     });
 
-    test('count: overwrites all children (uses overwrite mode)', async () => {
+    test('count: suppressed by default — no children are modified', async () => {
         const api = await createGrid('distribute-count', {
             aggFunc: 'count',
             groupRowValueSetter: distributeGroupValue,
@@ -192,32 +132,38 @@ describe.each(EDIT_MODES)('distributeGroupValue aggFunc strategies (%s)', (editM
         const franceNode = api.getRowNode('row-group-region-Europe-country-France')!;
         await performEdit(editMode, api, franceNode, 'amount', 77);
 
-        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(77);
-        expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(77);
+        // count is disabled by default — children unchanged
+        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(30);
+        expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(30);
+    });
 
-        await new GridRows(api, 'after count edit').check(`
-            ROOT id:ROOT_NODE_ID
-            ├─┬ filler id:row-group-region-Europe amount:{"value":6}
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:{"value":2}
-            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:77
-            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:77
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:{"value":2}
-            │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
-            │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
-            │ └─┬ LEAF_GROUP id:row-group-region-Europe-country-Italy amount:{"value":2}
-            │ · ├── LEAF id:it-rome region:"Europe" country:"Italy" amount:30
-            │ · └── LEAF id:it-milan region:"Europe" country:"Italy" amount:30
-            └─┬ filler id:row-group-region-Americas amount:{"value":4}
-            · ├─┬ LEAF_GROUP id:row-group-region-Americas-country-USA amount:{"value":2}
-            · │ ├── LEAF id:us-nyc region:"Americas" country:"USA" amount:70
-            · │ └── LEAF id:us-la region:"Americas" country:"USA" amount:30
-            · └─┬ LEAF_GROUP id:row-group-region-Americas-country-Canada amount:{"value":2}
-            · · ├── LEAF id:ca-toronto region:"Americas" country:"Canada" amount:35
-            · · └── LEAF id:ca-vancouver region:"Americas" country:"Canada" amount:25
-        `);
+    test('min with explicit per-aggFunc overwrite: writes to all children', async () => {
+        const api = await createGrid('distribute-min-overwrite', {
+            aggFunc: 'min',
+            groupRowValueSetter: { distribution: { min: 'overwrite' } },
+        });
+
+        const usaNode = api.getRowNode('row-group-region-Americas-country-USA')!;
+        await performEdit(editMode, api, usaNode, 'amount', 10);
+
+        expect(api.getRowNode('us-nyc')?.data?.amount).toBe(10);
+        expect(api.getRowNode('us-la')?.data?.amount).toBe(10);
+    });
+
+    test('max with explicit per-aggFunc overwrite: writes to all children', async () => {
+        const api = await createGrid('distribute-max-overwrite', {
+            aggFunc: 'max',
+            groupRowValueSetter: { distribution: { max: 'overwrite' } },
+        });
+
+        const usaNode = api.getRowNode('row-group-region-Americas-country-USA')!;
+        await performEdit(editMode, api, usaNode, 'amount', 100);
+
+        expect(api.getRowNode('us-nyc')?.data?.amount).toBe(100);
+        expect(api.getRowNode('us-la')?.data?.amount).toBe(100);
     });
 
-    test('custom aggFunc: defaults to overwrite', async () => {
+    test('function aggFunc: disabled by default — no children are modified', async () => {
         const api = await createGrid('distribute-custom-agg', {
             aggFunc: (params) => {
                 let total = 0;
@@ -232,15 +178,16 @@ describe.each(EDIT_MODES)('distributeGroupValue aggFunc strategies (%s)', (editM
         const franceNode = api.getRowNode('row-group-region-Europe-country-France')!;
         await performEdit(editMode, api, franceNode, 'amount', 50);
 
-        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(50);
-        expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(50);
+        // Function aggFuncs are disabled by default — children unchanged
+        expect(api.getRowNode('fr-paris')?.data?.amount).toBe(30);
+        expect(api.getRowNode('fr-lyon')?.data?.amount).toBe(30);
 
         await new GridRows(api, 'after custom agg edit').check(`
             ROOT id:ROOT_NODE_ID
-            ├─┬ filler id:row-group-region-Europe amount:880
-            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:200
-            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:50
-            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:50
+            ├─┬ filler id:row-group-region-Europe amount:720
+            │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-France amount:120
+            │ │ ├── LEAF id:fr-paris region:"Europe" country:"France" amount:30
+            │ │ └── LEAF id:fr-lyon region:"Europe" country:"France" amount:30
             │ ├─┬ LEAF_GROUP id:row-group-region-Europe-country-Germany amount:120
             │ │ ├── LEAF id:de-berlin region:"Europe" country:"Germany" amount:30
             │ │ └── LEAF id:de-hamburg region:"Europe" country:"Germany" amount:30
@@ -258,6 +205,24 @@ describe.each(EDIT_MODES)('distributeGroupValue aggFunc strategies (%s)', (editM
     });
 });
 
+describe('non-distributable aggFunc with invalid top-level strategy', () => {
+    test('max with top-level uniform: cell not editable and distribution suppressed', async () => {
+        const api = await createGrid('distribute-max-uniform', {
+            aggFunc: 'max',
+            groupRowValueSetter: { distribution: 'uniform' },
+        });
+
+        const usaNode = api.getRowNode('row-group-region-Americas-country-USA')!;
+        // Use setDataValue directly — the cell isn't editable in UI because isGroupCellEditable returns false
+        usaNode.setDataValue('amount', 100, 'ui');
+        await asyncSetTimeout(0);
+
+        // max is non-distributable — top-level 'uniform' doesn't enable it
+        expect(api.getRowNode('us-nyc')?.data?.amount).toBe(70);
+        expect(api.getRowNode('us-la')?.data?.amount).toBe(30);
+    });
+});
+
 describe('overriding built-in aggFunc with a custom function', () => {
     // Custom aggFunc registered under 'sum' that doubles the total
     const customSum = (params: any) => {
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-custom-io.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-custom-io.test.ts
index b8162e4dfff..31389a2f098 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-custom-io.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-custom-io.test.ts
@@ -128,16 +128,16 @@ describe('distributeGroupValue with custom getValue/setValue', () => {
         expect(api.getRowNode('a1')?.data?.amount).toBe(10);
     });
 
-    test('custom getValue with min/max and first strategy', async () => {
+    test('custom getValue with first aggFunc is suppressed by default', async () => {
         const api = await createSimpleGrid(
-            'custom-get-extremum',
+            'custom-get-first',
             [
                 { id: 'a1', region: 'R', country: 'C', amount: 5, score: 100 },
                 { id: 'a2', region: 'R', country: 'C', amount: 15, score: 50 },
                 { id: 'a3', region: 'R', country: 'C', amount: 25, score: 200 },
             ],
             {
-                aggFunc: 'min',
+                aggFunc: 'first',
                 groupRowValueSetter: (params) =>
                     distributeGroupValue(params, {
                         getValue: ({ node }) => node.data?.score ?? 0,
@@ -149,10 +149,10 @@ describe('distributeGroupValue with custom getValue/setValue', () => {
         groupNode.setDataValue('amount', 42, 'ui');
         await asyncSetTimeout(0);
 
-        // min strategy: writes to the child with the min getValue (score=50, which is a2)
-        expect(api.getRowNode('a1')?.data?.amount).toBe(5); // unchanged
-        expect(api.getRowNode('a2')?.data?.amount).toBe(42); // min holder gets new value
-        expect(api.getRowNode('a3')?.data?.amount).toBe(25); // unchanged
+        // first is non-distributable by default — children unchanged even with custom getValue
+        expect(api.getRowNode('a1')?.data?.amount).toBe(5);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(15);
+        expect(api.getRowNode('a3')?.data?.amount).toBe(25);
     });
 
     test('getValue/setValue receive api, context, node, colDef', async () => {
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-edge-cases.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-edge-cases.test.ts
index 8471deac3de..9ff640e0906 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-edge-cases.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-edge-cases.test.ts
@@ -1,3 +1,5 @@
+import type { GroupRowValueSetterParams } from 'ag-grid-community';
+
 import {
     GridRows,
     asyncSetTimeout,
@@ -16,20 +18,21 @@ describe('distributeGroupValue edge cases', () => {
     test('returns false for empty aggregatedChildren', () => {
         const result = distributeGroupValue({
             aggregatedChildren: [],
-            column: {} as any,
-            colDef: { aggFunc: 'sum' } as any,
+            column: {} as GroupRowValueSetterParams['column'],
+            colDef: { aggFunc: 'sum' } as GroupRowValueSetterParams['colDef'],
             newValue: 100,
             oldValue: 0,
+            data: undefined,
             eventSource: undefined,
             valueChanged: true,
-            node: {} as any,
-            api: {} as any,
+            node: {} as GroupRowValueSetterParams['node'],
+            api: {} as GroupRowValueSetterParams['api'],
             context: undefined,
         });
         expect(result).toBe(false);
     });
 
-    test('non-numeric value with first aggFunc sets only first child', async () => {
+    test('non-numeric value with first aggFunc is suppressed by default', async () => {
         const api = await gridsManager.createGridAndWait('distribute-non-numeric', {
             defaultColDef: { cellEditor: 'agTextCellEditor' },
             groupDisplayType: 'custom',
@@ -58,14 +61,15 @@ describe('distributeGroupValue edge cases', () => {
         groupNode.setDataValue('label', 'gamma', 'ui');
         await asyncSetTimeout(0);
 
-        expect(api.getRowNode('a1')?.data?.label).toBe('gamma');
+        // first is non-distributable by default — children unchanged
+        expect(api.getRowNode('a1')?.data?.label).toBe('alpha');
         expect(api.getRowNode('a2')?.data?.label).toBe('beta');
 
-        await new GridRows(api, 'after non-numeric edit').check(`
+        await new GridRows(api, 'after suppressed non-numeric edit').check(`
             ROOT id:ROOT_NODE_ID
-            └─┬ filler id:row-group-region-R label:"gamma"
-            · └─┬ LEAF_GROUP id:row-group-region-R-country-C label:"gamma"
-            · · ├── LEAF id:a1 region:"R" country:"C" label:"gamma"
+            └─┬ filler id:row-group-region-R label:"alpha"
+            · └─┬ LEAF_GROUP id:row-group-region-R-country-C label:"alpha"
+            · · ├── LEAF id:a1 region:"R" country:"C" label:"alpha"
             · · └── LEAF id:a2 region:"R" country:"C" label:"beta"
         `);
     });
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-numeric.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-numeric.test.ts
index 4d7f6ece06d..0a1ca828e2a 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-numeric.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-numeric.test.ts
@@ -1,3 +1,5 @@
+import type { GroupRowValueSetterParams } from 'ag-grid-community';
+
 import { asyncSetTimeout, createSimpleGrid, distributeGroupValue, gridsManager } from './distribute-test-utils';
 
 afterEach(() => {
@@ -99,36 +101,46 @@ describe('non-numeric newValue handling', () => {
 // --- Direct distributeGroupValue tests for value conversion (bypasses grid's data type checks) ---
 
 describe('numeric-like newValue handling (direct distributeGroupValue calls)', () => {
+    type MockChild = Pick<
+        GroupRowValueSetterParams['aggregatedChildren'][number],
+        'data' | 'getDataValue' | 'setDataValue'
+    >;
+
     /** Creates mock children that track writes via setDataValue. */
-    function mockChildren(values: number[]) {
+    function mockChildren(values: number[]): MockChild[] {
         return values.map((v, i) => {
             const data = { amount: v };
             return {
                 id: `child-${i}`,
                 data,
                 getDataValue: () => data.amount,
-                setDataValue: (_col: any, val: any) => {
+                setDataValue: (_col: GroupRowValueSetterParams['column'], val: unknown) => {
                     const old = data.amount;
-                    data.amount = val;
+                    data.amount = val as number;
                     return old !== val;
                 },
-            } as any;
+            };
         });
     }
 
-    function callDistribute(newValue: unknown, oldValue: unknown, children: any[], opts?: any) {
+    function callDistribute(
+        newValue: unknown,
+        oldValue: unknown,
+        children: MockChild[],
+        opts?: GroupRowValueSetterParams
+    ) {
         return distributeGroupValue(
             {
-                aggregatedChildren: children,
-                column: {} as any,
-                colDef: { aggFunc: 'sum' } as any,
+                aggregatedChildren: children as GroupRowValueSetterParams['aggregatedChildren'],
+                column: {} as GroupRowValueSetterParams['column'],
+                colDef: { aggFunc: 'sum' } as GroupRowValueSetterParams['colDef'],
                 newValue,
                 oldValue,
                 data: undefined,
                 eventSource: 'ui',
                 valueChanged: true,
-                node: {} as any,
-                api: {} as any,
+                node: {} as GroupRowValueSetterParams['node'],
+                api: {} as GroupRowValueSetterParams['api'],
                 context: undefined,
             },
             opts
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-options.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-options.test.ts
index a5c59b6e2c8..3e3e145560d 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-options.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-options.test.ts
@@ -100,6 +100,128 @@ describe('distributeGroupValue integer distribution via closure', () => {
     });
 });
 
+describe('defaultColDef merge with column-level groupRowValueSetter', () => {
+    test('defaultColDef percentage strategy is used when column record has undefined for sum', async () => {
+        const api = await gridsManager.createGridAndWait('merge-default-undefined', {
+            defaultColDef: {
+                cellEditor: 'agTextCellEditor',
+                editable: true,
+                groupRowEditable: true,
+                // defaultColDef sets percentage strategy for sum
+                groupRowValueSetter: { distribution: { sum: 'percentage' } },
+            },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'region', rowGroup: true, hide: true },
+                { field: 'country', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    // Column-level: sum is undefined → deep merge preserves defaultColDef's 'percentage'
+                    groupRowValueSetter: { distribution: { sum: undefined }, precision: 0 },
+                },
+            ],
+            rowData: [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+                { id: 'a3', region: 'R', country: 'C', amount: 30 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data?.id,
+        });
+
+        const groupNode = api.getRowNode('row-group-region-R-country-C')!;
+        groupNode.setDataValue('amount', 120, 'ui');
+        await asyncSetTimeout(0);
+
+        // defaultColDef's 'percentage' preserved → proportional distribution (10:20:30 = 1:2:3)
+        // 120 * 10/60 = 20, 120 * 20/60 = 40, 120 * 30/60 = 60
+        expect(api.getRowNode('a1')?.data?.amount).toBe(20);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(40);
+        expect(api.getRowNode('a3')?.data?.amount).toBe(60);
+    });
+
+    test('column-level explicit strategy overrides defaultColDef strategy', async () => {
+        const api = await gridsManager.createGridAndWait('merge-default-override', {
+            defaultColDef: {
+                cellEditor: 'agTextCellEditor',
+                editable: true,
+                groupRowEditable: true,
+                groupRowValueSetter: { distribution: 'percentage' },
+            },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'region', rowGroup: true, hide: true },
+                { field: 'country', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    // Column-level: explicit 'uniform' overrides default's 'percentage'
+                    groupRowValueSetter: { distribution: 'uniform' },
+                },
+            ],
+            rowData: [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data?.id,
+        });
+
+        const groupNode = api.getRowNode('row-group-region-R-country-C')!;
+        groupNode.setDataValue('amount', 60, 'ui');
+        await asyncSetTimeout(0);
+
+        // uniform: 60 / 2 = 30 each
+        expect(api.getRowNode('a1')?.data?.amount).toBe(30);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(30);
+    });
+
+    test('defaultColDef precision is inherited when column only sets distribution', async () => {
+        const api = await gridsManager.createGridAndWait('merge-default-precision', {
+            defaultColDef: {
+                cellEditor: 'agTextCellEditor',
+                editable: true,
+                groupRowEditable: true,
+                groupRowValueSetter: { precision: 0 },
+            },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'region', rowGroup: true, hide: true },
+                { field: 'country', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    // Column only sets distribution — precision: 0 inherited from defaultColDef
+                    groupRowValueSetter: { distribution: 'uniform' },
+                },
+            ],
+            rowData: [
+                { id: 'a1', region: 'R', country: 'C', amount: 10 },
+                { id: 'a2', region: 'R', country: 'C', amount: 10 },
+                { id: 'a3', region: 'R', country: 'C', amount: 10 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data?.id,
+        });
+
+        const groupNode = api.getRowNode('row-group-region-R-country-C')!;
+        groupNode.setDataValue('amount', 10, 'ui');
+        await asyncSetTimeout(0);
+
+        // 10 / 3 = 3.33, precision: 0 → [4, 3, 3]
+        expect(api.getRowNode('a1')?.data?.amount).toBe(4);
+        expect(api.getRowNode('a2')?.data?.amount).toBe(3);
+        expect(api.getRowNode('a3')?.data?.amount).toBe(3);
+    });
+});
+
 describe('distributeGroupValue with no aggFunc', () => {
     test('no aggFunc defaults to overwrite', async () => {
         const api = await gridsManager.createGridAndWait('distribute-no-agg', {
diff --git a/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts b/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts
index 1be5d45595c..38c47785657 100644
--- a/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/group-row-editable.test.ts
@@ -1,5 +1,8 @@
+import { userEvent } from '@testing-library/user-event';
+
 import type { GridOptions, ValueParserParams } from 'ag-grid-community';
 
+import { TestGridsManager } from '../../test-utils';
 import type {
     EditableCallback,
     GroupRowEditableCallback,
@@ -459,6 +462,133 @@ describe.each(EDIT_MODES)('groupRowEditable behaviour (%s)', (editMode) => {
         }
     });
 
+    test('double-clicking editable group cell starts editing instead of toggling expansion', async () => {
+        if (editMode !== 'ui') {
+            return; // dblclick only relevant for UI mode
+        }
+
+        const api = await gridsManager.createGridAndWait('group-dblclick-edit', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                {
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    editable: true,
+                    groupRowEditable: true,
+                },
+            ],
+            autoGroupColumnDef: {
+                editable: true,
+                groupRowEditable: true,
+                cellEditorParams: { useFormatter: true },
+            },
+            rowData: [
+                { id: 'a-1', category: 'A', amount: 10 },
+                { id: 'a-2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+
+        const gridDiv = TestGridsManager.getHTMLElement(api)!;
+        await asyncSetTimeout(1);
+
+        const groupRowNode = api.getDisplayedRowAtIndex(0);
+        expect(groupRowNode?.group).toBe(true);
+        expect(groupRowNode?.expanded).toBe(true);
+
+        // Double-click the amount cell — should start editing, NOT collapse
+        const rowElement = gridDiv.querySelector<HTMLElement>(`[row-id="${groupRowNode!.id}"]`)!;
+        const amountCell = rowElement.querySelector<HTMLElement>('[col-id="amount"]')!;
+        expect(amountCell).toBeTruthy();
+
+        await userEvent.dblClick(amountCell);
+        await asyncSetTimeout(1);
+
+        expect(groupRowNode?.expanded).toBe(true);
+        expect(api.getEditingCells()).toHaveLength(1);
+    });
+
+    test('double-clicking auto group column cell with groupRowEditable starts editing instead of toggling expansion', async () => {
+        if (editMode !== 'ui') {
+            return;
+        }
+
+        const api = await gridsManager.createGridAndWait('group-dblclick-autogroup', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                { field: 'amount', aggFunc: 'sum', editable: true, groupRowEditable: true },
+            ],
+            autoGroupColumnDef: {
+                editable: true,
+                groupRowEditable: true,
+                cellEditorParams: { useFormatter: true },
+            },
+            rowData: [
+                { id: 'a-1', category: 'A', amount: 10 },
+                { id: 'a-2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+
+        const gridDiv = TestGridsManager.getHTMLElement(api)!;
+        await asyncSetTimeout(1);
+
+        const groupRowNode = api.getDisplayedRowAtIndex(0);
+        expect(groupRowNode?.group).toBe(true);
+        expect(groupRowNode?.expanded).toBe(true);
+
+        // Double-click the auto group column cell — the groupCellRenderer dblclick handler
+        // should NOT toggle expansion when the cell is editable
+        const rowElement = gridDiv.querySelector<HTMLElement>(`[row-id="${groupRowNode!.id}"]`)!;
+        const autoGroupCell = rowElement.querySelector<HTMLElement>('[col-id="ag-Grid-AutoColumn"]')!;
+        expect(autoGroupCell).toBeTruthy();
+
+        await userEvent.dblClick(autoGroupCell);
+        await asyncSetTimeout(1);
+
+        expect(groupRowNode?.expanded).toBe(true);
+        expect(api.getEditingCells()).toHaveLength(1);
+    });
+
+    test('double-clicking non-editable auto group column cell toggles expansion normally', async () => {
+        if (editMode !== 'ui') {
+            return;
+        }
+
+        const api = await gridsManager.createGridAndWait('group-dblclick-collapse', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                { field: 'amount', aggFunc: 'sum' },
+            ],
+            rowData: [
+                { id: 'a-1', category: 'A', amount: 10 },
+                { id: 'a-2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+
+        const gridDiv = TestGridsManager.getHTMLElement(api)!;
+        await asyncSetTimeout(1);
+
+        const groupRowNode = api.getDisplayedRowAtIndex(0);
+        expect(groupRowNode?.group).toBe(true);
+        expect(groupRowNode?.expanded).toBe(true);
+
+        const rowElement = gridDiv.querySelector<HTMLElement>(`[row-id="${groupRowNode!.id}"]`)!;
+        const autoGroupCell = rowElement.querySelector<HTMLElement>('[col-id="ag-Grid-AutoColumn"]')!;
+        expect(autoGroupCell).toBeTruthy();
+
+        // Double-click on non-editable group cell should collapse
+        await userEvent.dblClick(autoGroupCell);
+        await asyncSetTimeout(1);
+
+        expect(groupRowNode?.expanded).toBe(false);
+        expect(api.getEditingCells()).toHaveLength(0);
+    });
+
     test('groupRowValueSetter fires even when groupRowEditable is false', async () => {
         let invocationCount = 0;
         const valueSetter: ValueSetterCallback = (params) => {
@@ -505,3 +635,435 @@ describe.each(EDIT_MODES)('groupRowEditable behaviour (%s)', (editMode) => {
         expect(invocationCount).toBe(1);
     });
 });
+
+describe('editability based on distribution configuration', () => {
+    const ROW_DATA = [
+        { id: 'a-1', category: 'A', amount: 10 },
+        { id: 'a-2', category: 'A', amount: 20 },
+    ];
+
+    test.each([
+        // --- Suppressed (not editable) ---
+        { name: 'distribution: false', aggFunc: 'sum', setter: { distribution: false }, expected: false },
+        { name: 'distribution: null', aggFunc: 'sum', setter: { distribution: null }, expected: false },
+        {
+            name: 'per-aggFunc record { sum: false }',
+            aggFunc: 'sum',
+            setter: { distribution: { sum: false, avg: 'uniform' } },
+            expected: false,
+        },
+        {
+            name: 'per-aggFunc options { sum: { distribution: false } }',
+            aggFunc: 'sum',
+            setter: { distribution: { sum: { distribution: false } } },
+            expected: false,
+        },
+        {
+            name: 'per-aggFunc count with distribution: undefined inherits disabled-by-default',
+            aggFunc: 'count',
+            setter: { distribution: { count: { distribution: undefined, precision: 0 } } },
+            expected: false,
+        },
+        {
+            name: 'per-aggFunc min without distribution key inherits disabled-by-default',
+            aggFunc: 'min',
+            setter: { distribution: { min: { precision: 0 } } },
+            expected: false,
+        },
+        { name: 'implicit count (no groupRowValueSetter)', aggFunc: 'count', expected: false },
+        { name: 'implicit min (no groupRowValueSetter)', aggFunc: 'min', expected: false },
+        { name: 'implicit max (no groupRowValueSetter)', aggFunc: 'max', expected: false },
+        { name: 'implicit first (no groupRowValueSetter)', aggFunc: 'first', expected: false },
+        { name: 'implicit last (no groupRowValueSetter)', aggFunc: 'last', expected: false },
+        {
+            name: 'groupRowValueSetter: true (count) → same as default',
+            aggFunc: 'count',
+            setter: true,
+            expected: false,
+        },
+        { name: 'groupRowValueSetter: true (min) → same as default', aggFunc: 'min', setter: true, expected: false },
+        { name: 'groupRowValueSetter: true (max) → same as default', aggFunc: 'max', setter: true, expected: false },
+        {
+            name: 'groupRowValueSetter: true (first) → same as default',
+            aggFunc: 'first',
+            setter: true,
+            expected: false,
+        },
+        { name: 'groupRowValueSetter: true (last) → same as default', aggFunc: 'last', setter: true, expected: false },
+        {
+            name: 'count with top-level distribution: true → same as default',
+            aggFunc: 'count',
+            setter: { distribution: true },
+            expected: false,
+        },
+        {
+            name: 'count with top-level distribution: "uniform" still disabled',
+            aggFunc: 'count',
+            setter: { distribution: 'uniform' },
+            expected: false,
+        },
+        {
+            name: 'min with top-level distribution: "percentage" still disabled',
+            aggFunc: 'min',
+            setter: { distribution: 'percentage' },
+            expected: false,
+        },
+        {
+            name: 'max with top-level distribution: "increment" still disabled',
+            aggFunc: 'max',
+            setter: { distribution: 'increment' },
+            expected: false,
+        },
+        {
+            name: 'implicit custom string aggFunc',
+            aggFunc: 'myAgg',
+            expected: false,
+            extra: {
+                aggFuncs: { myAgg: (params: any) => params.values.reduce((a: number, b: number) => a + b, 0) },
+            },
+        },
+        {
+            name: 'implicit function aggFunc',
+            aggFunc: ((params: any) => params.values.reduce((a: number, b: number) => a + b, 0)) as any,
+            expected: false,
+        },
+
+        // --- Editable ---
+        {
+            name: 'per-aggFunc record { sum: "percentage" }',
+            aggFunc: 'sum',
+            setter: { distribution: { sum: 'percentage', avg: false } },
+            expected: true,
+        },
+        {
+            name: 'per-aggFunc record missing matching aggFunc entry',
+            aggFunc: 'sum',
+            setter: { distribution: { avg: false } },
+            expected: true,
+        },
+        { name: 'groupRowValueSetter: true (sum)', aggFunc: 'sum', setter: true, expected: true },
+        { name: 'distribution: "uniform"', aggFunc: 'sum', setter: { distribution: 'uniform' }, expected: true },
+        {
+            name: 'per-aggFunc count with distribution: "overwrite" explicitly enabled',
+            aggFunc: 'count',
+            setter: { distribution: { count: { distribution: 'overwrite', precision: 0 } } },
+            expected: true,
+        },
+        {
+            name: 'per-aggFunc sum with distribution: undefined (sum not disabled by default)',
+            aggFunc: 'sum',
+            setter: { distribution: { sum: { distribution: undefined, precision: 0 } } },
+            expected: true,
+        },
+        { name: 'groupRowValueSetter as function', aggFunc: 'sum', setter: () => false, expected: true },
+        {
+            name: 'min with top-level distribution: "overwrite" → editable',
+            aggFunc: 'min',
+            setter: { distribution: 'overwrite' },
+            expected: true,
+        },
+        {
+            name: 'per-aggFunc max with overwrite → editable',
+            aggFunc: 'max',
+            setter: { distribution: { max: 'overwrite' } },
+            expected: true,
+        },
+        {
+            name: 'per-aggFunc first with true → editable',
+            aggFunc: 'first',
+            setter: { distribution: { first: true } },
+            expected: true,
+        },
+        {
+            name: 'per-aggFunc last with { distribution: "overwrite" } → editable',
+            aggFunc: 'last',
+            setter: { distribution: { last: { distribution: 'overwrite' } } },
+            expected: true,
+        },
+        { name: 'implicit sum (no groupRowValueSetter)', aggFunc: 'sum', expected: true },
+        { name: 'implicit avg (no groupRowValueSetter)', aggFunc: 'avg', expected: true },
+    ])('$name → editable=$expected', async ({ aggFunc, setter, expected, extra }: any) => {
+        const api = await gridsManager.createGridAndWait('editability-test', {
+            ...extra,
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                {
+                    field: 'amount',
+                    aggFunc,
+                    editable: true,
+                    groupRowEditable: true,
+                    ...(setter !== undefined ? { groupRowValueSetter: setter } : {}),
+                },
+            ],
+            rowData: ROW_DATA,
+            groupDefaultExpanded: -1,
+            getRowId: (params: any) => params.data.id,
+        });
+
+        const groupRowNode = api.getDisplayedRowAtIndex(0);
+        expect(groupRowNode?.group).toBe(true);
+        expect(api.getColumn('amount')!.isCellEditable(groupRowNode!)).toBe(expected);
+    });
+
+    test('leaf rows remain editable when group distribution is suppressed', async () => {
+        const api = await gridsManager.createGridAndWait('leaf-editable-check', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                {
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    editable: true,
+                    groupRowEditable: true,
+                    groupRowValueSetter: { distribution: false },
+                },
+            ],
+            rowData: ROW_DATA,
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+
+        const column = api.getColumn('amount')!;
+        const groupRowNode = api.getDisplayedRowAtIndex(0)!;
+        expect(column.isCellEditable(groupRowNode)).toBe(false);
+        expect(column.isCellEditable(api.getRowNode('a-1')!)).toBe(true);
+    });
+});
+
+describe('suppressDoubleClickExpand with groupRowEditable', () => {
+    test('double-click does not expand/collapse when suppressDoubleClickExpand is true', async () => {
+        const api = gridsManager.createGrid('suppress-dblclick-expand', {
+            defaultColDef: { cellEditor: 'agTextCellEditor' },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                {
+                    colId: 'group',
+                    headerName: 'Group',
+                    cellRenderer: 'agGroupCellRenderer',
+                    cellRendererParams: { suppressDoubleClickExpand: true },
+                },
+                { field: 'category', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    editable: true,
+                    groupRowEditable: true,
+                },
+            ],
+            rowData: [
+                { id: '1', category: 'A', amount: 10 },
+                { id: '2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+        await asyncSetTimeout(0);
+
+        const groupNode = api.getDisplayedRowAtIndex(0)!;
+        expect(groupNode.group).toBe(true);
+        expect(groupNode.expanded).toBe(true);
+
+        // Double-click the group cell — should NOT collapse when suppressDoubleClickExpand is true
+        const gridDiv = TestGridsManager.getHTMLElement(api)!;
+        const groupCell = gridDiv.querySelector<HTMLElement>(`[row-id="${groupNode.id}"] [col-id="group"]`);
+        expect(groupCell).not.toBeNull();
+
+        await userEvent.dblClick(groupCell!);
+        await asyncSetTimeout(0);
+
+        // Node should still be expanded — suppressDoubleClickExpand prevents collapse
+        expect(groupNode.expanded).toBe(true);
+    });
+});
+
+describe('groupRowValueSetter on columns without field or valueSetter', () => {
+    test('column with valueGetter and groupRowValueSetter (no field/valueSetter) allows group editing', async () => {
+        const groupRowValueSetterCalls: any[] = [];
+
+        const api = gridsManager.createGrid('no-field-groupRowValueSetter', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                { field: 'amount', aggFunc: 'sum', editable: true },
+                {
+                    colId: 'computed',
+                    headerName: 'Computed',
+                    valueGetter: ({ data }) => (data ? data.amount * 2 : null),
+                    aggFunc: 'sum',
+                    editable: false,
+                    groupRowEditable: true,
+                    groupRowValueSetter: (params) => {
+                        groupRowValueSetterCalls.push(params);
+                        const value = Number(params.newValue);
+                        if (!Number.isFinite(value)) {
+                            return false;
+                        }
+                        let changed = false;
+                        for (const child of params.aggregatedChildren) {
+                            if (child.setDataValue('amount', value, 'data')) {
+                                changed = true;
+                            }
+                        }
+                        return changed;
+                    },
+                },
+            ],
+            rowData: [
+                { id: '1', category: 'A', amount: 10 },
+                { id: '2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+        await asyncSetTimeout(0);
+
+        const groupNode = api.getDisplayedRowAtIndex(0)!;
+        expect(groupNode.group).toBe(true);
+
+        // Edit the computed column on the group row via UI
+        await editCell(api, groupNode, 'computed', '50');
+
+        // groupRowValueSetter should have been called
+        expect(groupRowValueSetterCalls).toHaveLength(1);
+        expect(groupRowValueSetterCalls[0].newValue).toBe('50');
+
+        // Children should have been updated
+        const child1 = api.getRowNode('1')!;
+        const child2 = api.getRowNode('2')!;
+        expect(child1.data.amount).toBe(50);
+        expect(child2.data.amount).toBe(50);
+    });
+
+    test('column with valueGetter, valueSetter, and groupRowValueSetter allows group editing', async () => {
+        const valueSetterCalls: any[] = [];
+        const groupRowValueSetterCalls: any[] = [];
+
+        const api = gridsManager.createGrid('valueSetter-groupRowValueSetter', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                { field: 'amount', editable: true },
+                {
+                    colId: 'computed',
+                    headerName: 'Computed',
+                    valueGetter: ({ data }) => (data ? data.amount * 2 : null),
+                    valueSetter: (params) => {
+                        valueSetterCalls.push(params);
+                        return true;
+                    },
+                    aggFunc: 'sum',
+                    editable: true,
+                    groupRowEditable: true,
+                    groupRowValueSetter: (params) => {
+                        groupRowValueSetterCalls.push(params);
+                        const value = Number(params.newValue);
+                        if (!Number.isFinite(value)) {
+                            return false;
+                        }
+                        let changed = false;
+                        for (const child of params.aggregatedChildren) {
+                            if (child.setDataValue('amount', value, 'data')) {
+                                changed = true;
+                            }
+                        }
+                        return changed;
+                    },
+                },
+            ],
+            rowData: [
+                { id: '1', category: 'A', amount: 10 },
+                { id: '2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+        await asyncSetTimeout(0);
+
+        const groupNode = api.getDisplayedRowAtIndex(0)!;
+        expect(groupNode.group).toBe(true);
+
+        // Edit the computed column on the group row
+        await editCell(api, groupNode, 'computed', '50');
+
+        // groupRowValueSetter should have been called, not valueSetter (group row)
+        expect(groupRowValueSetterCalls).toHaveLength(1);
+
+        // Children should have been updated
+        const child1 = api.getRowNode('1')!;
+        const child2 = api.getRowNode('2')!;
+        expect(child1.data.amount).toBe(50);
+        expect(child2.data.amount).toBe(50);
+
+        // Now edit a leaf row — should use valueSetter
+        valueSetterCalls.length = 0;
+        groupRowValueSetterCalls.length = 0;
+        await editCell(api, child1, 'computed', '99');
+
+        expect(valueSetterCalls).toHaveLength(1);
+        expect(groupRowValueSetterCalls).toHaveLength(0);
+    });
+
+    test('group row with null data does not call valueSetter, only groupRowValueSetter', async () => {
+        const valueSetterCalls: any[] = [];
+        const groupRowValueSetterCalls: any[] = [];
+
+        const api = gridsManager.createGrid('null-data-group', {
+            columnDefs: [
+                { field: 'category', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'sum',
+                    editable: true,
+                    valueSetter: (params) => {
+                        valueSetterCalls.push(params);
+                        if (params.data && params.colDef.field) {
+                            (params.data as Record<string, any>)[params.colDef.field] = params.newValue;
+                        }
+                        return true;
+                    },
+                    groupRowEditable: true,
+                    groupRowValueSetter: (params) => {
+                        groupRowValueSetterCalls.push(params);
+                        const value = Number(params.newValue);
+                        if (!Number.isFinite(value)) {
+                            return false;
+                        }
+                        let changed = false;
+                        for (const child of params.aggregatedChildren) {
+                            if (child.setDataValue(params.column, value, 'data')) {
+                                changed = true;
+                            }
+                        }
+                        return changed;
+                    },
+                },
+            ],
+            rowData: [
+                { id: '1', category: 'A', amount: 10 },
+                { id: '2', category: 'A', amount: 20 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data.id,
+        });
+        await asyncSetTimeout(0);
+
+        const groupNode = api.getDisplayedRowAtIndex(0)!;
+        expect(groupNode.group).toBe(true);
+        expect(groupNode.data).toBeUndefined();
+
+        // Edit the group row
+        await editCell(api, groupNode, 'amount', '50');
+
+        // groupRowValueSetter called, valueSetter NOT called for the group row
+        expect(groupRowValueSetterCalls).toHaveLength(1);
+        const groupValueSetterCallNodeIds = valueSetterCalls
+            .filter((c: any) => c.node?.group)
+            .map((c: any) => c.node.id);
+        expect(groupValueSetterCallNodeIds).toHaveLength(0);
+
+        // Children should have been updated
+        const child1 = api.getRowNode('1')!;
+        const child2 = api.getRowNode('2')!;
+        expect(child1.data.amount).toBe(50);
+        expect(child2.data.amount).toBe(50);
+    });
+});
diff --git a/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts b/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts
index d820f521810..2e3df9cfa21 100644
--- a/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts
+++ b/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts
@@ -1674,6 +1674,119 @@ describe('deferred column tool panel pivot mode', () => {
         expect(getApplyButton(toolPanelGui).disabled).toBe(false);
     });
 
+    test('apply button is disabled after reverting a staged row group change', async () => {
+        const { gridApi, toolPanel, toolPanelGui } = await createDeferredNonPivotGrid([
+            { field: 'country', enableRowGroup: true, rowGroup: true },
+            { field: 'athlete' },
+            { field: 'gold' },
+        ]);
+        const country = gridApi.getColumn('country')! as AgColumn;
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+
+        // Remove country from row groups
+        getUpdateStrategy(toolPanel).setRowGroupColumns(true, [], 'toolPanelUi');
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+        // Add country back to row groups (revert to original state)
+        getUpdateStrategy(toolPanel).setRowGroupColumns(true, [country], 'toolPanelUi');
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+    });
+
+    test('apply button stays enabled after removing then re-adding a row group column at a different position', async () => {
+        const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
+        const country = gridApi.getColumn('country')! as AgColumn;
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+
+        // Remove country from row groups via the drop zone
+        const rowGroupPanel = toolPanel.rowGroupDropZonePanel;
+        const existingRowGroupCols = rowGroupPanel.getExistingItems();
+        const withoutCountry = existingRowGroupCols.filter((c: AgColumn) => c !== country);
+        rowGroupPanel['updateItems'](withoutCountry);
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+        // Add country back at the end (different position from original)
+        const currentRowGroupCols = rowGroupPanel.getExistingItems();
+        rowGroupPanel['updateItems']([...currentRowGroupCols, country]);
+        toolPanel.refreshDeferredUi();
+
+        // Apply should stay enabled because order changed
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+    });
+
+    test('apply button is disabled after removing then re-adding a row group column at same position via drop zone', async () => {
+        const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
+        const sport = gridApi.getColumn('sport')! as AgColumn;
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+
+        // Step 1: Remove sport from row groups via the drop zone (simulates removing pill)
+        const rowGroupPanel = toolPanel.rowGroupDropZonePanel;
+        const originalRowGroupCols = [...rowGroupPanel.getExistingItems()];
+        const withoutSport = originalRowGroupCols.filter((c: AgColumn) => c !== sport);
+        rowGroupPanel['updateItems'](withoutSport);
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+        // Step 2: Drag sport back to row groups at same position (exact revert)
+        rowGroupPanel['updateItems'](originalRowGroupCols);
+        toolPanel.refreshDeferredUi();
+
+        // Apply should be disabled since we reverted to original state
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+    });
+
+    test('apply button is disabled after removing row group pill via X and dragging column back from list in pivot mode', async () => {
+        const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
+        const sport = gridApi.getColumn('sport')! as AgColumn;
+        const strategy = getUpdateStrategy(toolPanel);
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+
+        // Step 1: Click X on Sport pill in CTP row group drop zone
+        const rowGroupPanel = toolPanel.rowGroupDropZonePanel;
+        const withoutSport = rowGroupPanel.getExistingItems().filter((c: AgColumn) => c !== sport);
+        rowGroupPanel['updateItems'](withoutSport);
+        rowGroupPanel.refreshGui();
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+        // Step 2: Drag Sport from columns list into row groups drop zone
+        // handleDragEnterEnd sets column visibility to false (hide column on group)
+        rowGroupPanel.setColumnsVisible([sport], false, 'uiColumnDragged');
+        // addItem adds Sport back to row groups
+        rowGroupPanel.addItem(sport);
+        toolPanel.refreshDeferredUi();
+
+        // Deferred state should match live state — no pending changes
+        expect(strategy.hasPendingChanges(true)).toBe(false);
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+    });
+
+    test('apply button becomes enabled when row group columns are rearranged', async () => {
+        const { toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+
+        // Rearrange row group columns: swap country and sport
+        const rowGroupPanel = toolPanel.rowGroupDropZonePanel;
+        const originalRowGroupCols = [...rowGroupPanel.getExistingItems()];
+        const reversed = [...originalRowGroupCols].reverse();
+        rowGroupPanel['updateItems'](reversed);
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+    });
+
     test('apply button becomes enabled when a value column aggregation function is changed', async () => {
         const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
         const silver = gridApi.getColumn('silver')! as AgColumn;
diff --git a/testing/behavioural/src/drag-n-drop/tree-data/tree-data-managed-row-drag-basic.test.ts b/testing/behavioural/src/drag-n-drop/tree-data/tree-data-managed-row-drag-basic.test.ts
index 908a51d6261..fa837c002a2 100644
--- a/testing/behavioural/src/drag-n-drop/tree-data/tree-data-managed-row-drag-basic.test.ts
+++ b/testing/behavioural/src/drag-n-drop/tree-data/tree-data-managed-row-drag-basic.test.ts
@@ -31,7 +31,7 @@ describe.each([false, true])('tree data drag basics (suppress move %s)', (suppre
         treeDataChildrenField: 'children',
         rowDragManaged: true,
         suppressMoveWhenRowDragging,
-        rowDragInsertDelay: 30,
+        rowDragInsertDelay: 1,
         groupDefaultExpanded: -1,
         getRowId: ({ data }) => data.id,
     };
@@ -359,4 +359,65 @@ describe.each([false, true])('tree data drag basics (suppress move %s)', (suppre
         expect(api.getRowNode('project-alpha')?.parent?.id).toBe('projects');
         expect(api.getRowNode('alpha-design')?.parent?.id).toBe('project-alpha');
     });
+
+    test('clears child count when dragging the only child out of a group', async () => {
+        const rowData = [
+            {
+                id: 'backend',
+                name: 'Backend',
+                type: 'folder',
+                children: [
+                    { id: 'payment', name: 'Payment Integration', type: 'folder', children: [] },
+                    { id: 'user-auth', name: 'User Auth', type: 'folder', children: [] },
+                ],
+            },
+        ];
+
+        const api = createGrid('tree-managed-child-count', rowData);
+
+        const initialRows = new GridRows(api, 'initial');
+        await initialRows.check(`
+            ROOT id:ROOT_NODE_ID
+            └─┬ backend GROUP id:backend ag-Grid-AutoColumn:"Backend" type:"folder"
+            · ├── payment LEAF id:payment ag-Grid-AutoColumn:"Payment Integration" type:"folder"
+            · └── user-auth LEAF id:user-auth ag-Grid-AutoColumn:"User Auth" type:"folder"
+        `);
+
+        // Step 1: Drag 'User Auth' into 'Payment Integration' (making it a child)
+        const dispatcher1 = new RowDragDispatcher({ api });
+        await dispatcher1.start('user-auth');
+        await waitFor(() => expect(dispatcher1.getDragGhostLabel()).toBe('User Auth'));
+        await dispatcher1.move('payment', { center: true });
+        await asyncSetTimeout(10); // Wait for rowDragInsertDelay timer to fire and nudge
+        await dispatcher1.finish();
+        await asyncSetTimeout(0);
+
+        const afterDragIn = new GridRows(api, 'after drag into Payment');
+        await afterDragIn.check(`
+            ROOT id:ROOT_NODE_ID
+            └─┬ backend GROUP id:backend ag-Grid-AutoColumn:"Backend" type:"folder"
+            · └─┬ payment GROUP id:payment ag-Grid-AutoColumn:"Payment Integration" type:"folder"
+            · · └── user-auth LEAF id:user-auth ag-Grid-AutoColumn:"User Auth" type:"folder"
+        `);
+        expect(api.getRowNode('user-auth')?.parent?.id).toBe('payment');
+        expect(api.getRowNode('payment')?.allChildrenCount).toBe(1);
+
+        // Step 2: Drag 'User Auth' back out alongside 'Payment Integration'
+        const dispatcher2 = new RowDragDispatcher({ api });
+        await dispatcher2.start('user-auth');
+        await waitFor(() => expect(dispatcher2.getDragGhostLabel()).toBe('User Auth'));
+        await dispatcher2.move('payment', { yOffsetPercent: 0.05 });
+        await dispatcher2.finish();
+        await asyncSetTimeout(0);
+
+        const afterDragOut = new GridRows(api, 'after drag out of Payment');
+        await afterDragOut.check(`
+            ROOT id:ROOT_NODE_ID
+            └─┬ backend GROUP id:backend ag-Grid-AutoColumn:"Backend" type:"folder"
+            · ├── user-auth LEAF id:user-auth ag-Grid-AutoColumn:"User Auth" type:"folder"
+            · └── payment LEAF id:payment ag-Grid-AutoColumn:"Payment Integration" type:"folder"
+        `);
+        expect(api.getRowNode('user-auth')?.parent?.id).toBe('backend');
+        expect(api.getRowNode('payment')?.allChildrenCount).toBeNull();
+    });
 });
diff --git a/testing/behavioural/src/grouping-data/grouping-aggregation-call-count.test.ts b/testing/behavioural/src/grouping-data/grouping-aggregation-call-count.test.ts
new file mode 100644
index 00000000000..c635b8de657
--- /dev/null
+++ b/testing/behavioural/src/grouping-data/grouping-aggregation-call-count.test.ts
@@ -0,0 +1,186 @@
+import type { IAggFuncParams } from 'ag-grid-community';
+import { ClientSideRowModelModule } from 'ag-grid-community';
+import { RowGroupingModule } from 'ag-grid-enterprise';
+
+import { GridRows, TestGridsManager, cachedJSONObjects } from '../test-utils';
+
+describe('ag-grid aggregation call count with aggregateOnlyChangedColumns', () => {
+    const gridsManager = new TestGridsManager({
+        modules: [ClientSideRowModelModule, RowGroupingModule],
+    });
+
+    beforeEach(() => {
+        gridsManager.reset();
+    });
+
+    afterEach(() => {
+        gridsManager.reset();
+    });
+
+    // Reproduces the scenario from the change-detection-delta-aggregation docs example.
+    // Data: 2 top groups × 5 sub-groups × 3 leaves = 30 rows.
+    // Group tree: 2 top-level + 10 second-level = 12 group nodes.
+    // 4 value columns (a, b, c, d) with aggFunc → 4 × 12 = 48 aggregation calls on init.
+    // A Total column with only a valueGetter (no aggFunc) should NOT add extra calls.
+    test('initial aggregation call count is 4 columns × 12 groups = 48', async () => {
+        let aggCallCount = 0;
+        const countingSum = (params: IAggFuncParams) => {
+            aggCallCount++;
+            let result = 0;
+            for (const value of params.values) {
+                if (typeof value === 'number') {
+                    result += value;
+                }
+            }
+            return result;
+        };
+
+        const rowData = cachedJSONObjects.array(createRowData());
+
+        const api = gridsManager.createGrid('myGrid', {
+            columnDefs: [
+                { field: 'topGroup', rowGroup: true, hide: true },
+                { field: 'group', rowGroup: true, hide: true },
+                { field: 'a', aggFunc: countingSum },
+                { field: 'b', aggFunc: countingSum },
+                { field: 'c', aggFunc: countingSum },
+                { field: 'd', aggFunc: countingSum },
+                {
+                    headerName: 'Total',
+                    colId: 'total',
+                    valueGetter: 'getValue("a") + getValue("b") + getValue("c") + getValue("d")',
+                },
+            ],
+            autoGroupColumnDef: { minWidth: 180 },
+            animateRows: false,
+            groupDefaultExpanded: 1,
+            aggregateOnlyChangedColumns: true,
+            rowData,
+            getRowId: (params) => String(params.data.id),
+        });
+
+        // Verify the grid structure is correct
+        await new GridRows(api, 'initial', { columns: ['a', 'b', 'c', 'd'], checkDom: false }).check(`
+            ROOT id:ROOT_NODE_ID total:NaN
+            ├─┬ filler id:row-group-topGroup-Top ag-Grid-AutoColumn:"Top" a:770 b:690 c:770 d:730 total:2960
+            │ ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Top-group-Group A1" ag-Grid-AutoColumn:"Group A1" a:178 b:66 c:158 d:162 total:564
+            │ │ ├── LEAF hidden id:0 topGroup:"Top" group:"Group A1" a:63 b:11 c:43 d:77 total:194
+            │ │ ├── LEAF hidden id:1 topGroup:"Top" group:"Group A1" a:26 b:22 c:86 d:54 total:188
+            │ │ └── LEAF hidden id:2 topGroup:"Top" group:"Group A1" a:89 b:33 c:29 d:31 total:182
+            │ ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Top-group-Group A2" ag-Grid-AutoColumn:"Group A2" a:156 b:132 c:216 d:124 total:628
+            │ │ ├── LEAF hidden id:3 topGroup:"Top" group:"Group A2" a:26 b:22 c:86 d:54 total:188
+            │ │ ├── LEAF hidden id:4 topGroup:"Top" group:"Group A2" a:52 b:44 c:72 d:8 total:176
+            │ │ └── LEAF hidden id:5 topGroup:"Top" group:"Group A2" a:78 b:66 c:58 d:62 total:264
+            │ ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Top-group-Group A3" ag-Grid-AutoColumn:"Group A3" a:234 b:198 c:174 d:186 total:792
+            │ │ ├── LEAF hidden id:6 topGroup:"Top" group:"Group A3" a:89 b:33 c:29 d:31 total:182
+            │ │ ├── LEAF hidden id:7 topGroup:"Top" group:"Group A3" a:78 b:66 c:58 d:62 total:264
+            │ │ └── LEAF hidden id:8 topGroup:"Top" group:"Group A3" a:67 b:99 c:87 d:93 total:346
+            │ ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Top-group-Group A4" ag-Grid-AutoColumn:"Group A4" a:112 b:164 c:132 d:48 total:456
+            │ │ ├── LEAF hidden id:9 topGroup:"Top" group:"Group A4" a:52 b:44 c:72 d:8 total:176
+            │ │ ├── LEAF hidden id:10 topGroup:"Top" group:"Group A4" a:4 b:88 c:44 d:16 total:152
+            │ │ └── LEAF hidden id:11 topGroup:"Top" group:"Group A4" a:56 b:32 c:16 d:24 total:128
+            │ └─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Top-group-Group A5" ag-Grid-AutoColumn:"Group A5" a:90 b:130 c:90 d:210 total:520
+            │ · ├── LEAF hidden id:12 topGroup:"Top" group:"Group A5" a:15 b:55 c:15 d:85 total:170
+            │ · ├── LEAF hidden id:13 topGroup:"Top" group:"Group A5" a:30 b:10 c:30 d:70 total:140
+            │ · └── LEAF hidden id:14 topGroup:"Top" group:"Group A5" a:45 b:65 c:45 d:55 total:210
+            └─┬ filler id:row-group-topGroup-Bottom ag-Grid-AutoColumn:"Bottom" a:770 b:690 c:770 d:730 total:2960
+            · ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Bottom-group-Group B1" ag-Grid-AutoColumn:"Group B1" a:178 b:66 c:158 d:162 total:564
+            · │ ├── LEAF hidden id:15 topGroup:"Bottom" group:"Group B1" a:63 b:11 c:43 d:77 total:194
+            · │ ├── LEAF hidden id:16 topGroup:"Bottom" group:"Group B1" a:26 b:22 c:86 d:54 total:188
+            · │ └── LEAF hidden id:17 topGroup:"Bottom" group:"Group B1" a:89 b:33 c:29 d:31 total:182
+            · ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Bottom-group-Group B2" ag-Grid-AutoColumn:"Group B2" a:156 b:132 c:216 d:124 total:628
+            · │ ├── LEAF hidden id:18 topGroup:"Bottom" group:"Group B2" a:26 b:22 c:86 d:54 total:188
+            · │ ├── LEAF hidden id:19 topGroup:"Bottom" group:"Group B2" a:52 b:44 c:72 d:8 total:176
+            · │ └── LEAF hidden id:20 topGroup:"Bottom" group:"Group B2" a:78 b:66 c:58 d:62 total:264
+            · ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Bottom-group-Group B3" ag-Grid-AutoColumn:"Group B3" a:234 b:198 c:174 d:186 total:792
+            · │ ├── LEAF hidden id:21 topGroup:"Bottom" group:"Group B3" a:89 b:33 c:29 d:31 total:182
+            · │ ├── LEAF hidden id:22 topGroup:"Bottom" group:"Group B3" a:78 b:66 c:58 d:62 total:264
+            · │ └── LEAF hidden id:23 topGroup:"Bottom" group:"Group B3" a:67 b:99 c:87 d:93 total:346
+            · ├─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Bottom-group-Group B4" ag-Grid-AutoColumn:"Group B4" a:112 b:164 c:132 d:48 total:456
+            · │ ├── LEAF hidden id:24 topGroup:"Bottom" group:"Group B4" a:52 b:44 c:72 d:8 total:176
+            · │ ├── LEAF hidden id:25 topGroup:"Bottom" group:"Group B4" a:4 b:88 c:44 d:16 total:152
+            · │ └── LEAF hidden id:26 topGroup:"Bottom" group:"Group B4" a:56 b:32 c:16 d:24 total:128
+            · └─┬ LEAF_GROUP collapsed id:"row-group-topGroup-Bottom-group-Group B5" ag-Grid-AutoColumn:"Group B5" a:90 b:130 c:90 d:210 total:520
+            · · ├── LEAF hidden id:27 topGroup:"Bottom" group:"Group B5" a:15 b:55 c:15 d:85 total:170
+            · · ├── LEAF hidden id:28 topGroup:"Bottom" group:"Group B5" a:30 b:10 c:30 d:70 total:140
+            · · └── LEAF hidden id:29 topGroup:"Bottom" group:"Group B5" a:45 b:65 c:45 d:55 total:210
+        `);
+
+        // 4 value columns × 12 group nodes = 48 aggregation calls
+        expect(aggCallCount).toBe(48);
+    });
+
+    test('after single cell edit, only the affected column and path are re-aggregated', async () => {
+        let aggCallCount = 0;
+        const aggCallLog: string[] = [];
+        const countingSum = (params: IAggFuncParams) => {
+            aggCallCount++;
+            aggCallLog.push(params.column.getColId());
+            let result = 0;
+            for (const value of params.values) {
+                if (typeof value === 'number') {
+                    result += value;
+                }
+            }
+            return result;
+        };
+
+        const rowData = cachedJSONObjects.array(createRowData());
+
+        const api = gridsManager.createGrid('myGrid', {
+            columnDefs: [
+                { field: 'topGroup', rowGroup: true, hide: true },
+                { field: 'group', rowGroup: true, hide: true },
+                { field: 'a', aggFunc: countingSum },
+                { field: 'b', aggFunc: countingSum },
+                { field: 'c', aggFunc: countingSum },
+                { field: 'd', aggFunc: countingSum },
+                {
+                    headerName: 'Total',
+                    colId: 'total',
+                    valueGetter: 'getValue("a") + getValue("b") + getValue("c") + getValue("d")',
+                },
+            ],
+            autoGroupColumnDef: { minWidth: 180 },
+            animateRows: false,
+            groupDefaultExpanded: 1,
+            aggregateOnlyChangedColumns: true,
+            rowData,
+            getRowId: (params) => String(params.data.id),
+        });
+
+        // Reset counters after initial aggregation
+        aggCallCount = 0;
+        aggCallLog.length = 0;
+
+        // Update column 'c' for row id:0 (in Top → Group A1)
+        const rowNode = api.getRowNode('0')!;
+        rowNode.setDataValue('c', 99);
+
+        // With aggregateOnlyChangedColumns, only column 'c' should be re-aggregated,
+        // and only for the affected path: Group A1 and Top = 2 calls
+        expect(aggCallLog).toEqual(['c', 'c']);
+        expect(aggCallCount).toBe(2);
+    });
+});
+
+function createRowData() {
+    let id = 0;
+    const result: { id: number; topGroup: string; group: string; a: number; b: number; c: number; d: number }[] = [];
+    for (let i = 1; i <= 2; i++) {
+        for (let j = 1; j <= 5; j++) {
+            for (let k = 1; k <= 3; k++) {
+                result.push({
+                    id: id++,
+                    topGroup: i === 1 ? 'Top' : 'Bottom',
+                    group: (i === 1 ? 'Group A' : 'Group B') + j,
+                    a: (j * k * 863) % 100,
+                    b: (j * k * 811) % 100,
+                    c: (j * k * 743) % 100,
+                    d: (j * k * 677) % 100,
+                });
+            }
+        }
+    }
+    return result;
+}
diff --git a/testing/behavioural/src/selection/row-numbers-selection.test.ts b/testing/behavioural/src/selection/row-numbers-selection.test.ts
index b193e1a0b9f..8d28c61107b 100644
--- a/testing/behavioural/src/selection/row-numbers-selection.test.ts
+++ b/testing/behavioural/src/selection/row-numbers-selection.test.ts
@@ -277,6 +277,11 @@ describe('Row Numbers Keyboard Navigation', () => {
         return gridDiv.querySelector('.ag-aria-description-container')?.textContent ?? '';
     }
 
+    function getFocusedHeaderColId(): string | null {
+        const activeElement = document.activeElement as HTMLElement | null;
+        return activeElement?.closest('.ag-header-cell')?.getAttribute('col-id') ?? null;
+    }
+
     beforeAll(() => {
         setupAgTestIds();
     });
@@ -489,6 +494,7 @@ describe('Row Numbers Keyboard Navigation', () => {
         await asyncSetTimeout(0);
 
         assertSelectedCellRanges([{ rowStartIndex: 0, rowEndIndex: 3, columns: dataColumns }], api);
+        expect(getFocusedHeaderColId()).toBe(ROW_NUMBERS_COLUMN_ID);
     });
 
     test('Enter on row number header selects all cells', async () => {
@@ -508,6 +514,7 @@ describe('Row Numbers Keyboard Navigation', () => {
         await asyncSetTimeout(0);
 
         assertSelectedCellRanges([{ rowStartIndex: 0, rowEndIndex: 3, columns: dataColumns }], api);
+        expect(getFocusedHeaderColId()).toBe(ROW_NUMBERS_COLUMN_ID);
     });
 
     test('Space on row number cell does not select cells in that row', async () => {
diff --git a/testing/behavioural/src/sorting/benchmarks/delta-sort-transaction.bench.ts b/testing/behavioural/src/sorting/benchmarks/delta-sort-transaction.bench.ts
new file mode 100644
index 00000000000..1dd5da0b727
--- /dev/null
+++ b/testing/behavioural/src/sorting/benchmarks/delta-sort-transaction.bench.ts
@@ -0,0 +1,135 @@
+import type { BenchOptions } from 'vitest';
+import { bench, suite } from 'vitest';
+
+import type { GridApi } from 'ag-grid-community';
+import { ClientSideRowModelApiModule, ClientSideRowModelModule, ColumnApiModule } from 'ag-grid-community';
+
+import { SimplePRNG, TestGridsManager } from '../../test-utils';
+
+/**
+ * Benchmarks the delta-sorting example scenario:
+ * 100k rows, multi-column sort, applyTransaction with 1 add + 1 update.
+ *
+ * Mirrors: documentation/ag-grid-docs/src/content/docs/data-update-transactions/_examples/delta-sorting/
+ */
+
+interface IData {
+    id: number;
+    sort: number;
+    sort1: number;
+    sort2: number;
+}
+
+const ROW_COUNT = 100_000;
+
+function buildRowData(count: number, prng: SimplePRNG): IData[] {
+    const result = new Array<IData>(count);
+    for (let i = 0; i < count; i++) {
+        result[i] = {
+            id: i,
+            sort: prng.nextInt(2000, 2002),
+            sort1: prng.nextInt(2000, 2002),
+            sort2: prng.nextInt(2000, 102000),
+        };
+    }
+    return result;
+}
+
+suite('delta sort transactions (100k rows, multi-column sort)', () => {
+    const gridsManager = new TestGridsManager({
+        benchmark: true,
+        modules: [ClientSideRowModelModule, ClientSideRowModelApiModule, ColumnApiModule],
+    });
+
+    const prng = new SimplePRNG(0xde17a50);
+    const baseRowData = buildRowData(ROW_COUNT, prng);
+
+    let deltaSortApi!: GridApi<IData>;
+    let fullSortApi!: GridApi<IData>;
+    let nextId = ROW_COUNT;
+
+    const gridOptions = {
+        columnDefs: [
+            { field: 'id' as const },
+            { field: 'sort' as const, sortIndex: 0, sort: 'desc' as const },
+            { field: 'sort1' as const, sortIndex: 1, sort: 'desc' as const },
+            { field: 'sort2' as const, sortIndex: 2, sort: 'desc' as const },
+        ],
+        defaultColDef: { flex: 1 },
+        getRowId: ({ data }: { data: IData }) => String(data.id),
+    };
+
+    const benchOptions: BenchOptions = {
+        throws: true,
+        setup: () => {
+            nextId = ROW_COUNT;
+            deltaSortApi ??= gridsManager.createGrid('delta', {
+                ...gridOptions,
+                deltaSort: true,
+                rowData: baseRowData.slice(),
+            });
+            fullSortApi ??= gridsManager.createGrid('full', {
+                ...gridOptions,
+                deltaSort: false,
+                rowData: baseRowData.slice(),
+            });
+        },
+        teardown: () => {
+            gridsManager.reset();
+            deltaSortApi = undefined!;
+            fullSortApi = undefined!;
+        },
+    };
+
+    bench(
+        'applyTransaction (deltaSort: true) - 1 add + 1 update',
+        () => {
+            const id = nextId++;
+            deltaSortApi.applyTransaction({
+                add: [
+                    {
+                        id,
+                        sort: prng.nextInt(2000, 2002),
+                        sort1: prng.nextInt(2000, 2002),
+                        sort2: prng.nextInt(2000, 102000),
+                    },
+                ],
+                update: [
+                    {
+                        id: 1,
+                        sort: prng.nextInt(2000, 2002),
+                        sort1: prng.nextInt(2000, 2002),
+                        sort2: prng.nextInt(2000, 102000),
+                    },
+                ],
+            });
+        },
+        benchOptions
+    );
+
+    bench(
+        'applyTransaction (deltaSort: false) - 1 add + 1 update',
+        () => {
+            const id = nextId++;
+            fullSortApi.applyTransaction({
+                add: [
+                    {
+                        id,
+                        sort: prng.nextInt(2000, 2002),
+                        sort1: prng.nextInt(2000, 2002),
+                        sort2: prng.nextInt(2000, 102000),
+                    },
+                ],
+                update: [
+                    {
+                        id: 1,
+                        sort: prng.nextInt(2000, 2002),
+                        sort1: prng.nextInt(2000, 2002),
+                        sort2: prng.nextInt(2000, 102000),
+                    },
+                ],
+            });
+        },
+        benchOptions
+    );
+});
diff --git a/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts b/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts
index e74698e1464..7a2290ec0f4 100644
--- a/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts
+++ b/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts
@@ -219,6 +219,7 @@ export class GridRowsValidator {
         );
 
         this.validateSibling(rowErrors, row);
+        this.validatePinnedSibling(rowErrors, row);
 
         if (csrm) {
             const childrenAfterGroupSet = this.validateChildren(state, row, 'childrenAfterGroup', null);
@@ -336,6 +337,22 @@ export class GridRowsValidator {
         rowErrors.add(sibling.allLeafChildren !== row.allLeafChildren && 'Sibling allLeafChildren is different');
     }
 
+    private validatePinnedSibling(rowErrors: GridRowErrors, row: RowNode<any>) {
+        const pinnedSibling = row.pinnedSibling;
+        if (!pinnedSibling) {
+            return;
+        }
+        rowErrors.add(pinnedSibling === row && 'Row references itself as pinnedSibling');
+        rowErrors.add(
+            pinnedSibling.pinnedSibling !== row && 'PinnedSibling does not reference back to the original row'
+        );
+        rowErrors.add(pinnedSibling.group !== row.group && 'PinnedSibling group is different');
+        rowErrors.add(pinnedSibling.hasChildren() !== row.hasChildren() && 'PinnedSibling hasChildren() is different');
+        rowErrors.add(
+            pinnedSibling.allChildrenCount !== row.allChildrenCount && 'PinnedSibling allChildrenCount is different'
+        );
+    }
+
     private validateChildren(
         state: GridRowsValidationState,
         parentRow: RowNode,
diff --git a/testing/behavioural/src/tree-data/hierarchical/hierarchical-immutable-tree-data.test.ts b/testing/behavioural/src/tree-data/hierarchical/hierarchical-immutable-tree-data.test.ts
index 145f6d8fdf1..1e292995b30 100644
--- a/testing/behavioural/src/tree-data/hierarchical/hierarchical-immutable-tree-data.test.ts
+++ b/testing/behavioural/src/tree-data/hierarchical/hierarchical-immutable-tree-data.test.ts
@@ -888,6 +888,105 @@ describe('ag-grid hierarchical immutable tree data', () => {
         }
     });
 
+    test('allChildrenCount updates correctly through setRowData (0→1→2→1→0)', async () => {
+        const api = gridsManager.createGrid('myGrid', {
+            columnDefs: [{ field: 'v' }],
+            treeData: true,
+            treeDataChildrenField: 'children',
+            animateRows: false,
+            groupDefaultExpanded: -1,
+            rowData: cachedJSONObjects.array([
+                { id: 'parent', v: 'parent' },
+                { id: 'sibling', v: 'sibling' },
+            ]),
+            getRowId: ({ data }) => data.id,
+        });
+
+        // Initial: parent is a LEAF with no children (allChildrenCount = null)
+        let gridRows = new GridRows(api, '0 children');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├── parent LEAF id:parent ag-Grid-AutoColumn:"parent" v:"parent"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" v:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBeNull();
+
+        // Add first child via setRowData: parent becomes GROUP with allChildrenCount = 1
+        setRowDataChecked(
+            api,
+            cachedJSONObjects.array([
+                { id: 'parent', v: 'parent', children: [{ id: 'child-1', v: 'child-1' }] },
+                { id: 'sibling', v: 'sibling' },
+            ])
+        );
+        gridRows = new GridRows(api, '1 child');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├─┬ parent GROUP id:parent ag-Grid-AutoColumn:"parent" v:"parent"
+            │ └── child-1 LEAF id:child-1 ag-Grid-AutoColumn:"child-1" v:"child-1"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" v:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBe(1);
+
+        // Add second child: allChildrenCount = 2
+        setRowDataChecked(
+            api,
+            cachedJSONObjects.array([
+                {
+                    id: 'parent',
+                    v: 'parent',
+                    children: [
+                        { id: 'child-1', v: 'child-1' },
+                        { id: 'child-2', v: 'child-2' },
+                    ],
+                },
+                { id: 'sibling', v: 'sibling' },
+            ])
+        );
+        gridRows = new GridRows(api, '2 children');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├─┬ parent GROUP id:parent ag-Grid-AutoColumn:"parent" v:"parent"
+            │ ├── child-1 LEAF id:child-1 ag-Grid-AutoColumn:"child-1" v:"child-1"
+            │ └── child-2 LEAF id:child-2 ag-Grid-AutoColumn:"child-2" v:"child-2"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" v:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBe(2);
+
+        // Remove first child: allChildrenCount = 1
+        setRowDataChecked(
+            api,
+            cachedJSONObjects.array([
+                { id: 'parent', v: 'parent', children: [{ id: 'child-2', v: 'child-2' }] },
+                { id: 'sibling', v: 'sibling' },
+            ])
+        );
+        gridRows = new GridRows(api, '1 child again');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├─┬ parent GROUP id:parent ag-Grid-AutoColumn:"parent" v:"parent"
+            │ └── child-2 LEAF id:child-2 ag-Grid-AutoColumn:"child-2" v:"child-2"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" v:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBe(1);
+
+        // Remove last child: parent becomes LEAF, allChildrenCount = null
+        setRowDataChecked(
+            api,
+            cachedJSONObjects.array([
+                { id: 'parent', v: 'parent' },
+                { id: 'sibling', v: 'sibling' },
+            ])
+        );
+        gridRows = new GridRows(api, '0 children again');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├── parent LEAF id:parent ag-Grid-AutoColumn:"parent" v:"parent"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" v:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBeNull();
+    });
+
     test('suppressMaintainUnsortedOrder is respected', async () => {
         const rowData1 = cachedJSONObjects.array([
             {
diff --git a/testing/behavioural/src/tree-data/parentid/parentid-tree-transactions.test.ts b/testing/behavioural/src/tree-data/parentid/parentid-tree-transactions.test.ts
index 7f2f3fc3799..6e718c38ca0 100644
--- a/testing/behavioural/src/tree-data/parentid/parentid-tree-transactions.test.ts
+++ b/testing/behavioural/src/tree-data/parentid/parentid-tree-transactions.test.ts
@@ -104,6 +104,75 @@ describe('ag-grid tree transactions', () => {
         expect(gridRows.rootAllLeafChildren.map((row) => row.data)).toEqual([row0, row3, row4, row5b]);
     });
 
+    test('allChildrenCount updates correctly through add/remove transactions (0→1→2→1→0)', async () => {
+        const api = gridsManager.createGrid('myGrid', {
+            columnDefs: [{ field: 'x' }],
+            autoGroupColumnDef: { headerName: 'Hierarchy' },
+            treeData: true,
+            animateRows: false,
+            groupDefaultExpanded: -1,
+            rowData: [
+                { id: 'parent', x: 'parent' },
+                { id: 'sibling', x: 'sibling' },
+            ],
+            getRowId: (params) => params.data.id,
+            treeDataParentIdField: 'parentId',
+        });
+
+        // Initial: parent is a LEAF with no children (allChildrenCount = null)
+        let gridRows = new GridRows(api, '0 children');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├── parent LEAF id:parent ag-Grid-AutoColumn:"parent" x:"parent"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" x:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBeNull();
+
+        // Add first child: parent becomes GROUP with allChildrenCount = 1
+        applyTransactionChecked(api, { add: [{ id: 'child-1', x: 'child-1', parentId: 'parent' }] });
+        gridRows = new GridRows(api, '1 child');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├─┬ parent GROUP id:parent ag-Grid-AutoColumn:"parent" x:"parent"
+            │ └── child-1 LEAF id:child-1 ag-Grid-AutoColumn:"child-1" x:"child-1"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" x:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBe(1);
+
+        // Add second child: allChildrenCount = 2
+        applyTransactionChecked(api, { add: [{ id: 'child-2', x: 'child-2', parentId: 'parent' }] });
+        gridRows = new GridRows(api, '2 children');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├─┬ parent GROUP id:parent ag-Grid-AutoColumn:"parent" x:"parent"
+            │ ├── child-1 LEAF id:child-1 ag-Grid-AutoColumn:"child-1" x:"child-1"
+            │ └── child-2 LEAF id:child-2 ag-Grid-AutoColumn:"child-2" x:"child-2"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" x:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBe(2);
+
+        // Remove first child: allChildrenCount = 1
+        applyTransactionChecked(api, { remove: [{ id: 'child-1' }] });
+        gridRows = new GridRows(api, '1 child again');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├─┬ parent GROUP id:parent ag-Grid-AutoColumn:"parent" x:"parent"
+            │ └── child-2 LEAF id:child-2 ag-Grid-AutoColumn:"child-2" x:"child-2"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" x:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBe(1);
+
+        // Remove last child: parent becomes LEAF, allChildrenCount = null
+        applyTransactionChecked(api, { remove: [{ id: 'child-2' }] });
+        gridRows = new GridRows(api, '0 children again');
+        await gridRows.check(`
+            ROOT id:ROOT_NODE_ID
+            ├── parent LEAF id:parent ag-Grid-AutoColumn:"parent" x:"parent"
+            └── sibling LEAF id:sibling ag-Grid-AutoColumn:"sibling" x:"sibling"
+        `);
+        expect(api.getRowNode('parent')?.allChildrenCount).toBeNull();
+    });
+
     test('ag-grid parentId tree async complex transaction', async () => {
         const row0 = { id: '0', x: '0', parentId: null };
         const row1a = { id: '1', x: '1a', parentId: null };
diff --git a/testing/behavioural/src/version.ts b/testing/behavioural/src/version.ts
index 320ce849941..ff797fa4963 100644
--- a/testing/behavioural/src/version.ts
+++ b/testing/behavioural/src/version.ts
@@ -1,2 +1,2 @@
 // DO NOT UPDATE MANUALLY: Generated from script during build time
-export const VERSION = '35.1.0-beta.20260320.1044';
+export const VERSION = '35.2.0-beta.20260324.1227';
diff --git a/testing/csp/package.json b/testing/csp/package.json
index 94b276505b1..1204d572062 100644
--- a/testing/csp/package.json
+++ b/testing/csp/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-csp",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "CSP testing for AG Grid",
   "main": "index.js",
   "scripts": {},
diff --git a/testing/manual/template/vite.config.ts b/testing/manual/template/vite.config.ts
index 64d47e4ac83..a9720fdbf3c 100644
--- a/testing/manual/template/vite.config.ts
+++ b/testing/manual/template/vite.config.ts
@@ -16,7 +16,9 @@ export default defineConfig(async () => {
     const plugins = [cssFileDefaultImport(), ...(angularEnabled ? [] : [serveAngularDisabledPage()]), react(), vue()];
     const aliases: Record<string, string> = {
         'ag-grid-community': path.join(agGridRoot, 'packages/ag-grid-community/src/main.ts'),
+        'ag-grid-community/styles': path.join(agGridRoot, 'packages/ag-grid-community/styles'),
         'ag-grid-enterprise': path.join(agGridRoot, 'packages/ag-grid-enterprise/src/main.ts'),
+        'ag-grid-enterprise/styles': path.join(agGridRoot, 'packages/ag-grid-enterprise/styles'),
         'ag-grid-react': path.join(agGridRoot, 'packages/ag-grid-react/src/index.ts'),
         'ag-grid-vue3': path.join(agGridRoot, 'packages/ag-grid-vue3/src/main.ts'),
         '@': path.join(agGridRoot, 'packages/ag-grid-vue3/src'), // resolves @/ imports within ag-grid-vue3 source
@@ -43,11 +45,8 @@ export default defineConfig(async () => {
         console.log('\x1b[33m%s\x1b[0m', 'Angular disabled. Run with ANGULAR=1 yarn dev to enable.');
     }
 
-    const postcssPlugins = require(path.join(agGridRoot, 'postcss-plugins.cjs'));
-
     return {
         plugins,
-        css: { postcss: { plugins: postcssPlugins } },
         resolve: { alias: aliases },
         build: {
             target: 'esnext',
@@ -79,10 +78,14 @@ function serveAngularDisabledPage(): Plugin {
 }
 
 // AG Grid source uses `import css from './foo.css'` (default imports of CSS
-// strings). Vite requires `?inline` after the CSS file to get this behaviour.
-// This plugin appends ?inline to all imported CSS files.
+// strings). This plugin:
+// 1. Appends ?inline so Vite returns the CSS as a string (default export)
+// 2. Runs PostCSS on the CSS content (for RTL support etc) before Vite sees it
 function cssFileDefaultImport(): Plugin {
     const agGridSrc = path.join(agGridRoot, 'packages');
+    const postcssPlugins = require(path.join(agGridRoot, 'postcss-plugins.cjs'));
+    const postcss = require('postcss');
+
     return {
         name: 'ag-grid-css-inline',
         enforce: 'pre',
@@ -94,5 +97,16 @@ function cssFileDefaultImport(): Plugin {
                 }
             }
         },
+        async load(id) {
+            if (id.endsWith('.css?inline')) {
+                const realPath = id.slice(0, -'?inline'.length);
+                const raw = fs.readFileSync(realPath, 'utf-8');
+                const result = await postcss(postcssPlugins).process(raw, {
+                    from: realPath,
+                    to: realPath,
+                });
+                return result.css;
+            }
+        },
     };
 }
diff --git a/testing/module-size-angular/package.json b/testing/module-size-angular/package.json
index 526ce9c045e..46c7cfe4405 100644
--- a/testing/module-size-angular/package.json
+++ b/testing/module-size-angular/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ag-grid-module-size-angular",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "scripts": {
     "ng": "ng",
     "start": "ng serve",
@@ -20,11 +20,11 @@
     "@angular/platform-browser": "^19.0.0",
     "@angular/platform-browser-dynamic": "^19.0.0",
     "@angular/router": "^19.0.0",
-    "ag-grid-angular": "35.1.0-beta.20260320.1044",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
-    "ag-grid-enterprise": "35.1.0-beta.20260320.1044",
-    "ag-charts-community": "13.1.0-beta.20260320",
-    "ag-charts-enterprise": "13.1.0-beta.20260320",
+    "ag-grid-angular": "35.2.0-beta.20260324.1227",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
+    "ag-grid-enterprise": "35.2.0-beta.20260324.1227",
+    "ag-charts-community": "13.2.0-beta.20260324",
+    "ag-charts-enterprise": "13.2.0-beta.20260324",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"
diff --git a/testing/module-size/package.json b/testing/module-size/package.json
index 61b6c2536ed..3c4e419db27 100644
--- a/testing/module-size/package.json
+++ b/testing/module-size/package.json
@@ -1,7 +1,7 @@
 {
   "name": "ag-grid-module-size",
   "private": true,
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "scripts": {
     "dev": "vite",
     "cp-app": "cp ./src/App_Src.tsx ./src/App_AUTO.tsx",
@@ -14,11 +14,11 @@
     "test:e2e": "run-s \"module-combinations -- {1}\" module-validate --"
   },
   "dependencies": {
-    "ag-grid-react": "35.1.0-beta.20260320.1044",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
-    "ag-grid-enterprise": "35.1.0-beta.20260320.1044",
-    "ag-charts-community": "13.1.0-beta.20260320",
-    "ag-charts-enterprise": "13.1.0-beta.20260320",
+    "ag-grid-react": "35.2.0-beta.20260324.1227",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
+    "ag-grid-enterprise": "35.2.0-beta.20260324.1227",
+    "ag-charts-community": "13.2.0-beta.20260324",
+    "ag-charts-enterprise": "13.2.0-beta.20260324",
     "ag-shared": "0.0.1",
     "react": "^18.3.1",
     "react-dom": "^18.3.1"
diff --git a/testing/public-recipes/e2e/package.json b/testing/public-recipes/e2e/package.json
index aa04ffc6d4b..1f26b043a73 100644
--- a/testing/public-recipes/e2e/package.json
+++ b/testing/public-recipes/e2e/package.json
@@ -1,12 +1,12 @@
 {
   "name": "ag-grid-public-e2e-testing-recipes",
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "description": "Public E2E testing recipes for AG Grid",
   "main": "index.js",
   "scripts": {},
   "license": "MIT",
   "devDependencies": {
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
     "playwright": "^1.56.0",
     "@playwright/test": "^1.56.0",
     "@types/node": "^24.0.3"
diff --git a/testing/vue3-tests/package.json b/testing/vue3-tests/package.json
index 080887250ab..2eea193b32f 100644
--- a/testing/vue3-tests/package.json
+++ b/testing/vue3-tests/package.json
@@ -1,7 +1,7 @@
 {
   "name": "ag-grid-vue3-tests",
   "private": true,
-  "version": "35.1.0-beta.20260320.1044",
+  "version": "35.2.0-beta.20260324.1227",
   "type": "module",
   "scripts": {
     "dev": "vite",
@@ -15,9 +15,9 @@
   "dependencies": {
     "vue": "^3.5.0",
     "vue-router": "^4.5.0",
-    "ag-grid-community": "35.1.0-beta.20260320.1044",
-    "ag-grid-enterprise": "35.1.0-beta.20260320.1044",
-    "ag-grid-vue3": "35.1.0-beta.20260320.1044",
+    "ag-grid-community": "35.2.0-beta.20260324.1227",
+    "ag-grid-enterprise": "35.2.0-beta.20260324.1227",
+    "ag-grid-vue3": "35.2.0-beta.20260324.1227",
     "decimal.js": "^10.4.3"
   },
   "devDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 21d37043f27..eeeafaf1157 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9186,61 +9186,61 @@ adm-zip@^0.5.10:
   resolved "http://52.50.158.57:4873/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909"
   integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==
 
-ag-charts-angular@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-angular/-/ag-charts-angular-13.1.0-beta.20260320.tgz#5c878f85ccc2aaad8f3265cb755ac335ffcd6d35"
-  integrity sha512-h6CTXw+i+rQtOHne7YTgxzdI+ydyYZtDZbypfO4BsNcx0ECZ8ytXzJIkMsPGTWOFhxP+Xpzmrzh5kfi72i7JvQ==
+ag-charts-angular@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-angular/-/ag-charts-angular-13.2.0-beta.20260324.tgz#eca6840053672f7bf784f1fd3d852ac87d2f26c3"
+  integrity sha512-hV45dA/OBdnioULlFSOTHt7WEIV82F2WngKhOG9gdIyoKVmKPF2oMou02WpyvqiipOw+SisOMR8Nl05VwQVaXg==
   dependencies:
-    ag-charts-community "13.1.0-beta.20260320"
+    ag-charts-community "13.2.0-beta.20260324"
     tslib "^2.3.0"
 
-ag-charts-community@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-community/-/ag-charts-community-13.1.0-beta.20260320.tgz#3ea4c9c77cf8ff6b8b3493094a17795c63bf3ae3"
-  integrity sha512-kHltf/cgA1bZVl6cWzkxyPCjQAe8GivCE+4jkCNC+sFJ/UEK5gxLjtc7bbnpPClyjSVc0/GndLRrUmIuXYMXRw==
+ag-charts-community@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-community/-/ag-charts-community-13.2.0-beta.20260324.tgz#98fadb040edcf1e8b86433bc8ceb146c36174bc6"
+  integrity sha512-dFd6NwQVDx6zMBiK70kJVZj1CPJOvh3L74hpkdW0D5cs1EqRqXfW+p+P6Pf1wASzeX9D5aQLzI9xBsk10J9wvA==
   dependencies:
-    ag-charts-core "13.1.0-beta.20260320"
-    ag-charts-locale "13.1.0-beta.20260320"
-    ag-charts-types "13.1.0-beta.20260320"
+    ag-charts-core "13.2.0-beta.20260324"
+    ag-charts-locale "13.2.0-beta.20260324"
+    ag-charts-types "13.2.0-beta.20260324"
 
-ag-charts-core@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-core/-/ag-charts-core-13.1.0-beta.20260320.tgz#70e56e343c9df2584e0bbecb9c2200c396debd94"
-  integrity sha512-QD/EDo+umnCiIIUiIHUoewgep4yQaB/uJ5rN6827xGznkVGBtomeh1aTM+vJY5cbq2aUvARffHD1j4q2zPIDjw==
+ag-charts-core@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-core/-/ag-charts-core-13.2.0-beta.20260324.tgz#28f3cd2423ecd98e96743f9b357cc553e2dd2eba"
+  integrity sha512-SCjZEdDBOObGuzteiFtji3YjuZnM1hLAoX3cystrPT7TGbh2tJKsBlddJvk+gFeY7vUh5z3AmsrIc8c27FTkbg==
   dependencies:
-    ag-charts-types "13.1.0-beta.20260320"
+    ag-charts-types "13.2.0-beta.20260324"
 
-ag-charts-enterprise@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-enterprise/-/ag-charts-enterprise-13.1.0-beta.20260320.tgz#3e61b1b06f17632576a98330a68cda470053490e"
-  integrity sha512-eVOiizNGOefUwi+9IGoEbAWT++a8Ifq3q7xrALdYSroRzVNRDxLfMJN1ctT50wJIc+V4Jd0JE507h7Z7jOgaPw==
+ag-charts-enterprise@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-enterprise/-/ag-charts-enterprise-13.2.0-beta.20260324.tgz#6b5f9496d9437bcaa73d41c0d06c535b661cfa64"
+  integrity sha512-yzOK02LR9W+76JRtUg/fCRTT6eZuNF0szF/fZ7NCeFvMVXw6s/EyLP3bgKFFDFQYsrdof96mLs54td7KKYTuRA==
   dependencies:
-    ag-charts-community "13.1.0-beta.20260320"
-    ag-charts-core "13.1.0-beta.20260320"
+    ag-charts-community "13.2.0-beta.20260324"
+    ag-charts-core "13.2.0-beta.20260324"
 
-ag-charts-locale@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-locale/-/ag-charts-locale-13.1.0-beta.20260320.tgz#ce00f1119a1132a9adbc43893843b4945917a56a"
-  integrity sha512-Xio+Fmvo3GlqoAd64kIDnRtQyGsx6pHnIDXsLVQC7+mS1XcqsC00NiGn5i+UKvtdJvqACMIS2P3Yx7eqMtwSgA==
+ag-charts-locale@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-locale/-/ag-charts-locale-13.2.0-beta.20260324.tgz#e91451de5920fb6e9a58488cbbb9ceb85af29172"
+  integrity sha512-vabmnYLTt0waNC+pk5qht0AW8itncCSUQqkv/Xyx2SGJjCjYdMA8+TdoVnTdkIqdNOSnZWg2wHvRaJfWXavlLQ==
 
-ag-charts-react@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-react/-/ag-charts-react-13.1.0-beta.20260320.tgz#8f418249f208518352cd750feabd697dcba21767"
-  integrity sha512-ydt6zJ7XsKMSyoKJcheGChjr4D9R9vDKNQ1h4QLBMEn9F17cSPk8yyPnCTaFWSydR04ybFMtfp8mlO8DTVzrwg==
+ag-charts-react@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-react/-/ag-charts-react-13.2.0-beta.20260324.tgz#c167a30ab95ab37205b02c1987f10b25fb26d16c"
+  integrity sha512-etF+Wp78It2734aknpmgUFBZsc3KMpP+2sPIBxEP3Y4KlmhRUiRbIRXhZfUI77fCgh/EmD3EadveiIVViE6Kug==
   dependencies:
-    ag-charts-community "13.1.0-beta.20260320"
+    ag-charts-community "13.2.0-beta.20260324"
 
-ag-charts-types@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-types/-/ag-charts-types-13.1.0-beta.20260320.tgz#67729a0f7133dd652c25af9cfdc62bb4554eb878"
-  integrity sha512-EsjL66U9JYj4lqzH+P+vtRszPP836KXVwkK0aZddLAIMtqKktN8Wr2aLB8YNF1UVXiqGjDaAspiMyKdpNtmcTg==
+ag-charts-types@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-types/-/ag-charts-types-13.2.0-beta.20260324.tgz#26995f9a6762ff1c5a0a54554f0d6839e26859f8"
+  integrity sha512-OYjfEjeFbNJ48RJFJXWx95jGtZpuaLzIA1sm2xrxZGpoL2XpXMiecouP3s4pOrlJecInQj9RA3Lmq0aWKYWUQQ==
 
-ag-charts-vue3@13.1.0-beta.20260320:
-  version "13.1.0-beta.20260320"
-  resolved "https://registry.ag-grid.com/ag-charts-vue3/-/ag-charts-vue3-13.1.0-beta.20260320.tgz#567b41bad0444096057f66b34909b7a6a07104d5"
-  integrity sha512-V4uIExee16MdD1Cm6EXMXotPzCeFv2XAKjPpURXKo8HossjJXzFhHqEcQYxlH6s6pIz9ZVgGbcJEZvfc9Kp2Rg==
+ag-charts-vue3@13.2.0-beta.20260324:
+  version "13.2.0-beta.20260324"
+  resolved "https://registry.ag-grid.com/ag-charts-vue3/-/ag-charts-vue3-13.2.0-beta.20260324.tgz#98990dafeeb419e0ef169b26603b3f16ef45ec72"
+  integrity sha512-qD+H7WaSlFwbnRcoQyXdtLAotH1NBANe9ybEGiUHOw8owP+V4wwCyz0TjvKBFSHDJnVof/LWfj/0dlhR5MyuVA==
   dependencies:
-    ag-charts-community "13.1.0-beta.20260320"
+    ag-charts-community "13.2.0-beta.20260324"
 
 agent-base@6, agent-base@^6.0.2:
   version "6.0.2"
