of a page * * Attributes (all on the contents element except where noted): * wa-toc-element="contents" | "link" | "table" | "ix-trigger" * -- structural markers. Multiple * "table" elements are allowed * and each receives a TOC copy. * wa-toc-offsettop="" -- click-scroll offset only * (e.g. clear a fixed navbar) * wa-toc-offsetbottom="" -- shifts active line down * wa-toc-activeline="" -- where headings activate * during scroll. Default 33vh. * wa-toc-headings="" -- restrict which heading levels * are collected. Default h2..h6. * wa-toc-hideurlhash="true" -- suppress #hash on click * wa-toc-ancestoractive="true" -- mark ancestors active too * wa-toc-instance="" -- (on a shared parent) scopes * multiple TOCs on one page * * In-text directives (inside heading plain text, stripped at render): * [wa-toc-omit] -- exclude this heading * [wa-toc-h2] .. [wa-toc-h6] -- override heading level in * the TOC only * * Active state uses Webflow's native `.w--current` class so styling can be * configured directly in the Webflow Designer. */ (function () { 'use strict'; var ACTIVE_CLASS = 'w--current'; var ATTR = 'wa-toc-element'; var DIRECTIVE_RE = /\[wa-toc-(omit|h([2-6]))\]/i; var DEFAULT_HEADING_SELECTOR = 'h2, h3, h4, h5, h6'; // ---------- utilities -------------------------------------------------- /** * Slugify heading text into an id. Mirrors Webflow's native id generation * closely enough for hash navigation: lowercase, spaces 鈫 hyphens, drop * non-alphanumeric except hyphens, collapse runs of hyphens. */ function slugify(text) { return String(text) .toLowerCase() .trim() .replace(/[\s_]+/g, '-') .replace(/[^a-z0-9\-]/g, '') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } /** Ensure a slug is unique within a Set, suffixing -2, -3, ... if needed. */ function uniqueSlug(base, used) { if (!base) base = 'section'; var slug = base; var i = 2; while (used.has(slug)) { slug = base + '-' + i++; } used.add(slug); return slug; } /** * Convert a CSS length string (e.g. "8rem", "120px", "10vh") to pixels, * resolved against the document root. Returns 0 on failure. */ function cssLengthToPx(value) { if (!value) return 0; var probe = document.createElement('div'); probe.style.position = 'absolute'; probe.style.visibility = 'hidden'; probe.style.height = value; probe.style.width = '0'; document.body.appendChild(probe); var px = probe.getBoundingClientRect().height; document.body.removeChild(probe); return isFinite(px) ? px : 0; } /** * Parse the wa-toc-headings attribute into a CSS selector. Accepts: * "h2,h3" 鈫 "h2, h3" * "h2 h3 h4" 鈫 "h2, h3, h4" * "2,3" 鈫 "h2, h3" * "H2, h3" 鈫 "h2, h3" (case-insensitive) * Tokens outside h2..h6 are dropped. Returns the default selector when * the input is empty or yields no valid levels 鈥 keeps behavior safe. */ function parseHeadingLevels(raw) { if (!raw) return DEFAULT_HEADING_SELECTOR; var tokens = String(raw) .toLowerCase() .split(/[\s,]+/); var levels = []; var seen = {}; for (var i = 0; i < tokens.length; i++) { var tok = tokens[i].trim(); if (!tok) continue; // Accept "h2" or "2". var m = /^h?([2-6])$/.exec(tok); if (!m) continue; var tag = 'h' + m[1]; if (seen[tag]) continue; seen[tag] = true; levels.push(tag); } if (!levels.length) return DEFAULT_HEADING_SELECTOR; return levels.join(', '); } /** Find the closest ancestor (or self) carrying wa-toc-instance. */ function instanceOf(el) { var node = el; while (node && node.nodeType === 1) { if (node.hasAttribute('wa-toc-instance')) { return node.getAttribute('wa-toc-instance'); } node = node.parentElement; } return ''; // default unnamed instance } /** Find the link template within a given root, matching wa-toc-element="link". */ function findLinkTemplate(root) { return root.querySelector('[' + ATTR + '="link"]'); } /** * The link template may be applied directly to an , or to a text * element nested inside an . Resolve to the actual we should clone. */ function resolveAnchorTemplate(linkEl) { if (linkEl.tagName === 'A') return linkEl; var anchor = linkEl.closest('a'); return anchor || linkEl; // fall back to the marked element itself } /** * Within a cloned link, find the text-bearing element that originally had * wa-toc-element="link". If the marker was on the anchor itself, the * anchor is the text holder. */ function findTextHolder(clonedAnchor, originalMarker) { if (originalMarker.tagName === 'A') return clonedAnchor; // The marker was a descendant; find the same descendant in the clone by // walking the same path. var path = []; var node = originalMarker; var rootAnchor = originalMarker.closest('a'); while (node && node !== rootAnchor) { var parent = node.parentElement; if (!parent) break; path.unshift(Array.prototype.indexOf.call(parent.children, node)); node = parent; } var current = clonedAnchor; for (var i = 0; i < path.length; i++) { current = current.children[path[i]]; if (!current) return clonedAnchor; } return current; } // ---------- per-instance pipeline ------------------------------------- function buildInstance(contentsEl) { var instanceId = instanceOf(contentsEl); // Locate the link template in the same instance. var linkMarker = null; var allLinks = document.querySelectorAll('[' + ATTR + '="link"]'); for (var i = 0; i < allLinks.length; i++) { if (instanceOf(allLinks[i]) === instanceId) { linkMarker = allLinks[i]; break; } } if (!linkMarker) return null; // nothing to do var anchorTemplate = resolveAnchorTemplate(linkMarker); // Locate every wa-toc-element="table" in the same instance. The TOC will // be rendered into each one 鈥 same headings, same active-state tracking, // independent styling contexts. If none are present, fall back to a // single mount at the link template's parent (Finsweet-compatible). var mountEls = []; var allTables = document.querySelectorAll('[' + ATTR + '="table"]'); for (var j = 0; j < allTables.length; j++) { if (instanceOf(allTables[j]) === instanceId) { mountEls.push(allTables[j]); } } if (!mountEls.length) { var fallback = anchorTemplate.parentElement; if (!fallback) return null; mountEls.push(fallback); } // Read config off the contents element. var offsetTopVal = cssLengthToPx(contentsEl.getAttribute('wa-toc-offsettop')); var offsetBotVal = cssLengthToPx(contentsEl.getAttribute('wa-toc-offsetbottom')); var hideHash = contentsEl.getAttribute('wa-toc-hideurlhash') === 'true'; var ancestorMode = contentsEl.getAttribute('wa-toc-ancestoractive') === 'true'; var activeLineAttr = contentsEl.getAttribute('wa-toc-activeline'); var activeLineRaw = activeLineAttr; // keep raw for vh re-resolution on resize var headingSelector = parseHeadingLevels(contentsEl.getAttribute('wa-toc-headings')); // Collect heading elements in document order, applying directives. var rawHeadings = contentsEl.querySelectorAll(headingSelector); var headings = []; var usedIds = new Set(); for (var k = 0; k < rawHeadings.length; k++) { var h = rawHeadings[k]; var text = h.textContent || ''; var levelOverride = null; var omit = false; // Strip *all* directive occurrences from the rendered heading text and // capture the first relevant one. We also clear them from the live DOM // so readers don't see "[wa-toc-h4]My heading". var cleaned = text; var match; while ((match = DIRECTIVE_RE.exec(cleaned))) { if (match[1].toLowerCase() === 'omit') { omit = true; } else if (match[2]) { levelOverride = parseInt(match[2], 10); } cleaned = cleaned.slice(0, match.index) + cleaned.slice(match.index + match[0].length); } cleaned = cleaned.trim(); if (cleaned !== text) { // Replace directive markers in text nodes only, preserving inline // markup like / children. stripDirectivesInPlace(h); } if (omit || !cleaned) continue; // Assign / preserve id for hash linking. var id = h.id; if (!id) { id = uniqueSlug(slugify(cleaned), usedIds); h.id = id; } else { usedIds.add(id); } var actualLevel = parseInt(h.tagName.charAt(1), 10); var displayLevel = levelOverride || actualLevel; headings.push({ el: h, id: id, text: cleaned, level: displayLevel, }); } if (!headings.length) return null; // The TOC must start at H2. If somebody overrode levels, normalize so the // shallowest level encountered renders at the outermost nesting depth. var minLevel = headings.reduce(function (m, h) { return Math.min(m, h.level); }, 6); // Build the nested DOM. We need *wrappers* so children of an H# can be // appended next to (not inside) the link itself. Per Finsweet's docs: // "Each Heading link template must be enclosed in a div wrapper, and // this div wrapper should be a child of the div associated with the // preceding Heading level." // // We model this with a per-level "wrapper" element. The first child of // the wrapper is the link; subsequent children are nested wrappers. var rootContainer = document.createElement('div'); rootContainer.setAttribute('wa-toc-element', 'list'); var entries = []; // parallel array: { link, wrapper, heading } var stack = [{ level: minLevel - 1, wrapper: rootContainer }]; for (var n = 0; n < headings.length; n++) { var heading = headings[n]; // Pop until the top of stack is the parent level (one shallower). while (stack.length > 1 && stack[stack.length - 1].level >= heading.level) { stack.pop(); } var parentWrapper = stack[stack.length - 1].wrapper; // Build wrapper for this heading. var wrapper = document.createElement('div'); wrapper.setAttribute('wa-toc-element', 'h' + heading.level + '-wrapper'); // Clone the link template. var link = anchorTemplate.cloneNode(true); // Remove the marker attribute on the clone to avoid re-detection. var markersInClone = link.querySelectorAll('[' + ATTR + '="link"]'); for (var mi = 0; mi < markersInClone.length; mi++) { markersInClone[mi].removeAttribute(ATTR); } if (link.getAttribute(ATTR) === 'link') link.removeAttribute(ATTR); // Set the text on the resolved text holder. var holder = findTextHolder(link, linkMarker); holder.textContent = heading.text; // Set href and tag with entry index so we can find this same link // inside each cloned mount and wire it up. link.setAttribute('href', '#' + heading.id); link.setAttribute('data-wa-toc-idx', String(entries.length)); wrapper.appendChild(link); parentWrapper.appendChild(wrapper); entries.push({ heading: heading, // Filled in below: one cloned link per mount target. links: [], ixTriggers: [], }); stack.push({ level: heading.level, wrapper: wrapper }); } // Mount the built tree into every target. We clone for *every* mount // (including the first) so `rootContainer` stays intact across the loop // and serves as a clean source for each subsequent mount. Querying // inside each mount by data-wa-toc-idx lets us collect the per-mount // link nodes back into the corresponding entry. function mountInto(target, treeChildren) { while (target.firstChild) target.removeChild(target.firstChild); for (var c = 0; c < treeChildren.length; c++) target.appendChild(treeChildren[c]); // Pull this mount's links into entries. var mountedLinks = target.querySelectorAll('[data-wa-toc-idx]'); for (var ml = 0; ml < mountedLinks.length; ml++) { var idx = parseInt(mountedLinks[ml].getAttribute('data-wa-toc-idx'), 10); if (entries[idx]) entries[idx].links.push(mountedLinks[ml]); // Per Finsweet ix-trigger semantics: triggers live inside the link // template, fire on this entry's active transitions. var triggers = mountedLinks[ml].querySelectorAll('[' + ATTR + '="ix-trigger"]'); for (var tg = 0; tg < triggers.length; tg++) { entries[idx].ixTriggers.push(triggers[tg]); } } } for (var t = 0; t < mountEls.length; t++) { var cloned = rootContainer.cloneNode(true); var children = Array.prototype.slice.call(cloned.children); mountInto(mountEls[t], children); } // Strip the temporary index marker now that all mounts have been wired. for (var en = 0; en < entries.length; en++) { for (var lk = 0; lk < entries[en].links.length; lk++) { entries[en].links[lk].removeAttribute('data-wa-toc-idx'); } } // Click handling: smooth scroll, hash control. Listener runs in capture // phase and stops propagation so any external listener (e.g. Webflow's // own anchor handling, or a parent click handler on the link wrapper) // can't run a competing scroll. We also reread offsetTopVal on each // click in case the page has resized 鈥 vh-based offsets need this. function onLinkClick(entry, ev) { ev.preventDefault(); ev.stopPropagation(); var liveOffsetTop = cssLengthToPx(contentsEl.getAttribute('wa-toc-offsettop')); var targetTop = entry.heading.el.getBoundingClientRect().top + window.pageYOffset - liveOffsetTop; window.scrollTo({ top: targetTop, behavior: 'smooth' }); if (!hideHash) { if (history.replaceState) { history.replaceState(null, '', '#' + entry.heading.id); } else { location.hash = '#' + entry.heading.id; } } } entries.forEach(function (entry) { entry.links.forEach(function (link) { link.addEventListener( 'click', function (ev) { onLinkClick(entry, ev); }, true, ); }); }); return { contentsEl: contentsEl, entries: entries, offsetTop: offsetTopVal, offsetBottom: offsetBotVal, activeLineRaw: activeLineRaw, ancestorMode: ancestorMode, activeIndex: -1, previousIxState: new WeakMap(), }; } /** * Walk text nodes inside `el` and remove [wa-toc-omit] / [wa-toc-h#] markers * so they don't appear in the rendered page. */ function stripDirectivesInPlace(el) { var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); var node; var toFix = []; while ((node = walker.nextNode())) { if (DIRECTIVE_RE.test(node.nodeValue)) toFix.push(node); } toFix.forEach(function (n) { n.nodeValue = n.nodeValue.replace(/\[wa-toc-(omit|h[2-6])\]/gi, '').trim(); }); } // ---------- active-link tracking -------------------------------------- /** * Default active-line position when wa-toc-activeline isn't set. * 1/3 of viewport height 鈥 a heading activates as soon as it scrolls up * into the upper third of the viewport, while still comfortably visible. */ var DEFAULT_ACTIVE_LINE_FRACTION = 1 / 3; /** * Resolve the active-line position (in px from viewport top) for an * instance. wa-toc-activeline accepts any CSS length; if absent, falls * back to a fraction of viewport height. Recomputed on every call so * vh-based values track resizes naturally. */ function resolveActiveLine(instance) { if (instance.activeLineRaw) { var px = cssLengthToPx(instance.activeLineRaw); if (px > 0) return px; } var viewportH = window.innerHeight || document.documentElement.clientHeight; return viewportH * DEFAULT_ACTIVE_LINE_FRACTION; } /** * Determine which heading owns the viewport. * * Rule: the active heading is the *last* one whose top has crossed the * "active line" 鈥 a horizontal line in the viewport configured by * wa-toc-activeline (default: 1/3 of viewport height). * * `wa-toc-offsetbottom` extends the line further down 鈥 i.e. the previous * heading stays active until the next heading reaches `offsetBottom` past * the active line. * * Note: this is independent of `wa-toc-offsettop`, which only affects * smooth-scroll-on-click (so a fixed navbar can be cleared without * shifting where headings activate). * * Entries are scanned in document order and we take the maximum match * rather than break early, so the result is correct even if CSS reorders * elements relative to DOM order. * * Returns -1 when no heading has yet reached the active line. */ function computeActiveIndex(instance) { var entries = instance.entries; if (!entries.length) return -1; var threshold = resolveActiveLine(instance) + instance.offsetBottom; var active = -1; for (var i = 0; i < entries.length; i++) { // viewport-relative top of this heading var top = entries[i].heading.el.getBoundingClientRect().top; if (top <= threshold) { active = i; // keep the latest match 鈥 do NOT break } } return active; } /** * Apply active state. Always clears every link first so external sources * of `.w--current` (e.g. Webflow's URL-hash matching) can't leave stale * classes around. We don't early-out on `newIndex === activeIndex` for * that same reason 鈥 it's cheap to re-set and guarantees correctness. * * Each entry may have multiple link clones (one per mount target). The * active class is applied uniformly to all clones so every rendered TOC * stays in sync. */ function applyActive(instance, newIndex) { var entries = instance.entries; function setClass(entry, on) { for (var li = 0; li < entry.links.length; li++) { if (on) entry.links[li].classList.add(ACTIVE_CLASS); else entry.links[li].classList.remove(ACTIVE_CLASS); } } // Clear all. for (var i = 0; i < entries.length; i++) setClass(entries[i], false); if (newIndex >= 0 && newIndex < entries.length) { // Mark the directly-active entry. var activeEntry = entries[newIndex]; setClass(activeEntry, true); // Optionally mark ancestors. An ancestor of entry N is the most recent // earlier entry whose level is strictly shallower, walking up until we // run out of shallower levels. if (instance.ancestorMode) { var currentLevel = activeEntry.heading.level; for (var k = newIndex - 1; k >= 0 && currentLevel > 1; k--) { if (entries[k].heading.level < currentLevel) { setClass(entries[k], true); currentLevel = entries[k].heading.level; } } } } // Fire ix-trigger transitions only when the index actually changed. We // dispatch on every cloned trigger across all mounts so Webflow IX2 // animations run uniformly in each rendered TOC. if (newIndex !== instance.activeIndex) { for (var j = 0; j < entries.length; j++) { var entry = entries[j]; if (!entry.ixTriggers || !entry.ixTriggers.length) continue; // Read state from the first clone 鈥 they're kept in sync. var isActive = entry.links.length > 0 && entry.links[0].classList.contains(ACTIVE_CLASS); var was = instance.previousIxState.get(entry) === true; if (isActive !== was) { entry.ixTriggers.forEach(function (t) { t.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); }); instance.previousIxState.set(entry, isActive); } } instance.activeIndex = newIndex; } } // ---------- bootstrap -------------------------------------------------- function init() { var contentsEls = document.querySelectorAll('[' + ATTR + '="contents"]'); var instances = []; contentsEls.forEach(function (el) { var inst = buildInstance(el); if (inst) instances.push(inst); }); if (!instances.length) return; function update() { for (var i = 0; i < instances.length; i++) { applyActive(instances[i], computeActiveIndex(instances[i])); } } // Throttle to one update per frame. var ticking = false; function onScroll() { if (ticking) return; ticking = true; requestAnimationFrame(function () { update(); ticking = false; }); } window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); update(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();

成人VR视频

Skip to main content

The AI impact gap: Why law firms are failing at AI implementation

By:
Lisa Burtch
September 9, 2026
Lisa Burtch
,
Tom Snavely
September 9, 2026
Tom Snavely
,
Eric Lein
September 9, 2026
Eric Lein
,
September 10, 2026
September 10, 2026
11 min

This is the first in a three-part series from the 成人VR视频 Transformation Services team exploring how law firms can move beyond AI aspiration and toward measurable impact.

Key takeaways

  • 91% of professionals say their organizations are falling short of AI's potential value delivery.
  • 78% of clients see AI-enabled quality improvements as essential, yet only 6% believe providers deliver.
  • Successful firms treat AI implementation as strategic discipline, not just technology procurement.
The following scenario is a composite drawn from patterns observed across multiple client engagements.

The managing partner and leadership team thought they had done everything right. They invested in cutting-edge AI tools. The partnership vote was unanimous. The firmwide announcement email highlighted leadership鈥檚 commitment to innovation. Expectations were clearly set around required training and appropriate use.

Six months later, the usage reports told a different story. Only a fraction of attorneys had logged into the AI tools more than once. Despite the worrying data, no meaningful action was taken until the firm lost an important opportunity with a valued client. The firm鈥檚 team told the client it would take weeks and tens of thousands of dollars in research time just to develop an initial strategy. Meanwhile, a competing firm delivered a preliminary case assessment quickly using AI analysis, outlined strategic options, estimated costs for each of those options, and identified key risks before the engagement letter was signed. The opportunity wasn鈥檛 lost because the firm lacked AI tools. It was lost because those tools were unused while competitors had already reimagined how they delivered value.

When the firm dug deeper, they found that a quiet rebellion had been underway for months. Lawyers complained that no one had trained them on the new tools beyond a single 90-minute webinar. Partners admitted they didn鈥檛 understand how the AI tools worked and felt uneasy discussing anything to do with AI with clients.

This scenario is unfolding in firms around the world and reflects a growing disconnect across the legal profession. While law firm executives debate market positioning and competitive advantage, their own professionals are struggling with a different reality. AI adoption is no longer the obstacle, but 91% of professionals say their organizations are falling short of what the technology could deliver, a shortfall the 成人VR视频 2026 Future of Professionals Report labels an 鈥淎I value gap.鈥

Even where an AI strategy exists, execution is lagging. Thirty-five percent of professionals say ambitions are not reflected in their day-to-day work, and one in four say they would consider leaving within two years if they don鈥檛 see the value they expect. Clients are reaching the same conclusion: 78% now see AI-enabled quality improvements as essential, yet just 6% believe most providers are delivering, and 32% have reconsidered or plan within 12 months to reconsider relationships with firms they view as falling behind. This gap between leadership vision and daily experience represents more than a missed opportunity. It is an existential risk that may determine which firms thrive and which become cautionary tales.

The illusion of AI impact

Too many law firm leaders are operating under a fundamental misunderstanding of their organization鈥檚 true AI impact. They see technology budgets approved, vendor contracts signed, and training sessions scheduled and assume transformation is underway. The reality inside the firm often tells a very different story.

Leadership frequently interprets activity as progress, even when daily behavior does not reflect strategic accomplishment. Attorneys may remain uncertain about how the tools work, practice groups may not adapt their workflows, and partners may hesitate to introduce AI into client conversations. This mismatch creates an illusion of impact that masks deeper capability gaps.

The consequences extend far beyond internal inefficiency. Forty-six percent of lawyers report that they do not know enough about AI to answer basic questions posed by their clients about its potential benefits. This gap highlights a growing disconnect between executive enthusiasm for AI and the level of understanding among practicing lawyers.

Much of this disconnect stems from a focus on procurement rather than capability building. Buying technology is relatively simple. Changing habits, workflows, and client engagement practices is far more challenging, and many firms underestimate the investment required to support that shift.

The message is clear. Access to the latest technology does not guarantee value for law firms. As the legal profession reaches an inflection point, the firms most likely to succeed are not those with the largest technology budgets, but those willing to assess whether their attorneys are to use these tools effectively.

The innovation leaders: What they know that others don鈥檛

The formula for successful firms is not just buying better technology. They are approaching AI implementation as a strategic discipline rather than a technology project, and that difference consistently separates leaders from the rest of the market.

These leading firms understand a critical truth: AI success depends not only on the sophistication of the tools they buy but also on how accurately leadership understands the current state of their organization. Leadership invests time in developing a clear, shared understanding of the firm鈥檚 current state before setting expectations or defining success. Decisions are made with explicit strategic intent, grounded in how AI will support client service, practice priorities, and competitive positioning.

Just as importantly, innovation leaders treat implementation as an ongoing process rather than a milestone. They regularly evaluate whether expected benefits are being realized, identify gaps between intention and execution, and adjust accordingly. This ability to measure, reflect, and course correct allows them to translate ambition into sustained performance rather than isolated success.

Understanding how these firms operate provides a practical reference point for firms seeking to move from aspiration to .

Moving beyond the illusion

The window for incremental AI adoption is closing rapidly. The firms that will dominate the next era of legal practice are those whose leaders insist on a clear, evidence-based view of impact rather than assuming technology procurement equals transformation.

In the second part of our series, we鈥檒l explore the five critical dimensions successful firms use to assess their and identify exactly where capability, confidence, and alignment are still missing.

Because in a market where innovation increasingly shapes client satisfaction, revenue growth, and competitive positioning, honest assessment has become a business imperative rather than a strategic ambition.

You can find out more about the challenges AI poses to the legal profession here

Follow us on social

Have questions?

Get in touch with one of our solutions experts....
成人VR视频 Institute logo

Featured Event

Table of
Contents
H2
H3
H4
H5
H6
September 10, 2026
The AI impact gap: Why law firms are failing at AI implementation
This is the first in a three-part series from the 成人VR视频 Transformation Services team exploring how law firms can move beyond AI aspiration and toward measurable impact.
11 min
September 10, 2026
Legal AI & Technology
The AI impact gap: Why law firms are failing at AI implementation
Lisa Burtch
Project Specialist
成人VR视频 Institute
Tom Snavely
Principal in Transformation Services
成人VR视频 Institute
Eric Lein
Business Process Analyst
成人VR视频 Institute
Law Firm Marketing
Law Firm Profitability
Recruiting & hiring
Lawyer Staffing & Headcount
Agentic AI
Generative AI
AI literacy
Professional Development
Legal professionals
AI & Future of Professions
How AI is hollowing out the legal profession's judgment pipeline 鈥 and how to fix it
Premortem: Your 2028 agentic AI pilot program failed
The 2030 legal department: 5 ways AI will transform how in-house teams work