← Home
package main

import (
	"bufio"
	"fmt"
	"os"
)

const MOD int64 = 998244353

func addmod(a, b int64) int64 { a += b; if a >= MOD { a -= MOD }; return a }
func submod(a, b int64) int64 { a -= b; if a < 0 { a += MOD }; return a }
func mulmod(a, b int64) int64 { return (a * b) % MOD }
func powmod(a, e int64) int64 {
	res := int64(1)
	for e > 0 {
		if e&1 == 1 {
			res = (res * a) % MOD
		}
		a = (a * a) % MOD
		e >>= 1
	}
	return res
}
func invmod(a int64) int64 { return powmod(a, MOD-2) }

func main() {
	in := bufio.NewReader(os.Stdin)
	var n, m, k int
	if _, err := fmt.Fscan(in, &n, &m, &k); err != nil {
		return
	}

	type Arr struct {
		c int
		v []int
	}
	arrs := make([]Arr, n)
	appearsGe2 := make([]bool, k+1)
	dupInSome := make([]bool, k+1)
	for i := 0; i < n; i++ {
		var c int
		fmt.Fscan(in, &c)
		arrs[i].c = c
		arrs[i].v = make([]int, c)
		cnt := make(map[int]int)
		for j := 0; j < c; j++ {
			fmt.Fscan(in, &arrs[i].v[j])
			cnt[arrs[i].v[j]]++
		}
		if c >= 2 {
			for _, x := range arrs[i].v {
				appearsGe2[x] = true
			}
		}
		dup := false
		for _, t := range cnt {
			if t > 1 {
				dup = true
				break
			}
		}
		if dup {
			for x := range cnt {
				dupInSome[x] = true
			}
		}
	}

	// Build directed graph for arrays with c>=2 and without duplicates
	out := make([]int, k+1)
	inDeg := make([]int, k+1)
	for i := 1; i <= k+1; i++ {
		if i <= k {
			out[i] = 0
			inDeg[i] = 0
		}
	}
	hasEdge := make([]bool, k+1)
	bad := make([]bool, k+1)

	for i := 0; i < n; i++ {
		c := arrs[i].c
		if c < 2 {
			continue
		}
		// Check duplicates again
		ok := true
		seen := make(map[int]bool)
		for _, x := range arrs[i].v {
			if seen[x] {
				ok = false
				break
			}
			seen[x] = true
		}
		if !ok {
			continue
		}
		for j := 0; j+1 < c; j++ {
			u := arrs[i].v[j]
			v := arrs[i].v[j+1]
			// add edge u->v; but if conflicting multiple outgoing/incoming, mark bad
			if out[u] == 0 {
				out[u] = v
			} else if out[u] != v {
				bad[u] = true
				bad[out[u]] = true
				bad[v] = true
			}
			if out[u] != 0 {
				hasEdge[u] = true
			}
			inDeg[v]++
			if inDeg[v] > 1 {
				bad[v] = true
			}
		}
	}

	// Any node appearing in any duplicate array is bad (forbidden)
	for x := 1; x <= k; x++ {
		if dupInSome[x] {
			bad[x] = true
		}
	}

	// Detect cycles among nodes with out[u]!=0
	vis := make([]int8, k+1) // 0 unvisited, 1 visiting, 2 done
	var dfs func(u int)
	cycleNodes := make([]bool, k+1)
	dfs = func(u int) {
		vis[u] = 1
		v := out[u]
		if v != 0 {
			if vis[v] == 0 {
				dfs(v)
			} else if vis[v] == 1 {
				// cycle detected; mark cycle nodes
				// walk until come back
				x := v
				for {
					cycleNodes[x] = true
					x = out[x]
					if x == v || x == 0 {
						break
					}
				}
			}
		}
		vis[u] = 2
	}
	for u := 1; u <= k; u++ {
		if hasEdge[u] && vis[u] == 0 {
			dfs(u)
		}
	}
	for u := 1; u <= k; u++ {
		if cycleNodes[u] {
			bad[u] = true
		}
	}

	// Good components: path segments consisting of nodes with out possibly 0/1, inDeg<=1, not bad, and connected by edges
	usedInEdges := make([]bool, k+1)
	for u := 1; u <= k; u++ {
		if hasEdge[u] || inDeg[u] > 0 {
			usedInEdges[u] = true
		}
	}
	visitedComp := make([]bool, k+1)

	compSizes := make([]int, 0)
	for u := 1; u <= k; u++ {
		if usedInEdges[u] && !bad[u] {
			// find path start: inDeg==0 or predecessor bad/none
			if inDeg[u] == 0 {
				// walk path
				v := u
				sz := 0
				ok := true
				for v != 0 && usedInEdges[v] && !visitedComp[v] {
					if bad[v] {
						ok = false
						break
					}
					visitedComp[v] = true
					sz++
					v = out[v]
				}
				if ok {
					compSizes = append(compSizes, sz)
				}
			}
		}
	}
	// There may be remaining nodes that are not visited but have inDeg>0 (part of paths whose starts are bad or missing)
	for u := 1; u <= k; u++ {
		if usedInEdges[u] && !bad[u] && !visitedComp[u] {
			// This should be a path piece with cycle or entering from bad; walk and mark but don't count
			v := u
			for v != 0 && usedInEdges[v] && !visitedComp[v] {
				visitedComp[v] = true
				v = out[v]
			}
			// do not add as good component
		}
	}

	// Count free letters: not participating in any c>=2 array and not in any duplicate array
	free := 0
	for x := 1; x <= k; x++ {
		if !appearsGe2[x] && !dupInSome[x] {
			free++
		}
	}

	// Precompute factorials up to k (sufficient for (free + number of components))
	maxN := k
	fact := make([]int64, maxN+5)
	ifact := make([]int64, maxN+5)
	fact[0] = 1
	for i := 1; i <= maxN+4; i++ {
		fact[i] = mulmod(fact[i-1], int64(i))
	}
	ifact[maxN+4] = invmod(fact[maxN+4])
	for i := maxN + 4; i >= 1; i-- {
		ifact[i-1] = mulmod(ifact[i], int64(i))
	}
	comb := func(n, r int) int64 {
		if r < 0 || r > n {
			return 0
		}
		return mulmod(fact[n], mulmod(ifact[r], ifact[n-r]))
	}

	// divisors of m
	divs := make([]int, 0)
	for d := 1; d*d <= m; d++ {
		if m%d == 0 {
			divs = append(divs, d)
			if d*d != m {
				divs = append(divs, m/d)
			}
		}
	}

	// base answer: all strings using only free letters (no constraints)
	ans := powmod(int64(free), int64(m))

	// Add contributions by taking ALL good components together (approximation)
	sumComp := 0
	for _, s := range compSizes {
		sumComp += s
	}
	B := len(compSizes)

	// If there are no good components, nothing extra beyond free^m is safely counted here
	if B > 0 {
		for _, L := range divs {
			// Try to form period length L by using all components plus r free letters
			if L < sumComp {
				continue
			}
			r := L - sumComp
			if r > free || r < 0 {
				continue
			}
			// strings exist only if m % L == 0
			if m%L != 0 {
				continue
			}
			ways := comb(free, r)
			blocks := B + r
			ways = mulmod(ways, fact[blocks])
			ans = addmod(ans, ways)
		}
	}

	fmt.Println(ans % MOD)
}